Compare commits

..

52 Commits

Author SHA1 Message Date
ad7ad1fa78 release: 2024.8.2 2024-09-16 14:13:04 +02:00
c70e609e50 website/docs: prepare release notes for 2024.8.2 (#11394)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	website/docs/releases/2024/v2024.8.md
2024-09-16 14:12:28 +02:00
5f08485fff web: revert lockfile lint, re-add integrity (#11380)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/package-lock.json
2024-09-14 23:16:56 +02:00
3a2ed11821 providers/proxy: fix URL path getting lost when partial URL is given to rd= (cherry-pick #11354) (#11355)
providers/proxy: fix URL path getting lost when partial URL is given to rd= (#11354)

* providers/proxy: fix URL path getting lost when partial URL is given to rd=



* better fallback + tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-12 18:58:47 +02:00
ee04f39e28 enterprise: fix API mixin license validity check (cherry-pick #11331) (#11342)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
fix API mixin license validity check (#11331)
2024-09-11 13:22:01 +00:00
2c6aa72f3c sources/ldap: fix missing search attribute (cherry-pick #11125) (#11340)
sources/ldap: fix missing search attribute (#11125)

* unrelated



* sources/ldap: fix ldap sync not requesting uniqueness attribute



* check object_uniqueness_field for none



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-11 14:03:12 +02:00
bd0afef790 enterprise: show specific error if Install ID is invalid in license (cherry-pick #11317) (#11319)
enterprise: show specific error if Install ID is invalid in license (#11317)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-10 19:38:45 +02:00
fc11cc0a1a core: fix permission check for scoped impersonation (cherry-pick #11315) (#11316)
core: fix permission check for scoped impersonation (#11315)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-10 14:19:30 +02:00
fb78303e8f web/admin: fix notification property mapping forms (cherry-pick #11298) (#11300)
web/admin: fix notification property mapping forms (#11298)

* fix incorrect base class



* fix doclink url

closes #11276



* fix sidebar order in website



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-09 19:27:29 +02:00
2ea04440db events: optimise marking events as seen (cherry-pick #11297) (#11299)
events: optimise marking events as seen (#11297)

* events: optimise marking events as seen



* add tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-09 19:26:43 +02:00
96e1636be3 core: ensure all providers have correct priority (cherry-pick #11280) (#11281)
core: ensure all providers have correct priority (#11280)

follow up to #11267 which broke SAML lookup

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-08 16:09:09 +02:00
c546451a73 root: fix ensure `outpost_connection_discovery runs on worker startup (cherry-pick #11260) (#11270)
root: fix ensure `outpost_connection_discovery runs on worker startup (#11260)

* root: fix ensure outpost_connection_discovery runs on worker startup

Make outpost_connection_discovery a startup task for default_tenant to ensure it's ran during worker startup. Without this waiting for the 8 hour schedule to fire is required.

fixes: https://github.com/goauthentik/authentik/issues/10933



* format



---------

Signed-off-by: Anthony Rabbito <arabbito@coreweave.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Anthony Rabbito <hello@anthonyrabbito.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-09-07 21:54:30 +02:00
61778053b4 core: ensure proxy provider is correctly looked up (cherry-pick #11267) (#11269)
core: ensure proxy provider is correctly looked up (#11267)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-07 21:53:30 +02:00
f5580d311d release: 2024.8.1 2024-09-07 16:14:54 +02:00
99d292bce0 web/users: show - if device was registered before we started saving the time (cherry-pick #11256) (#11257)
web/users: show - if device was registered before we started saving the time (#11256)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-06 21:13:03 +02:00
b2801641bc internal: fix go paginator not setting page correctly (cherry-pick #11253) (#11255)
internal: fix go paginator not setting page correctly (#11253)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-06 18:46:18 +02:00
bfaa1046b2 core: fix missing argument name escaping for property mapping (cherry-pick #11231) (#11252)
core: fix missing argument name escaping for property mapping (#11231)

* escape property mapping args



* improve display of error



* fix error handling, missing dry_run argument



* use different sanitisation



* update docs



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-06 16:47:27 +02:00
95c30400cc providers/ldap: rework search_group migration to work with read replicas (cherry-pick #11228) (#11229)
providers/ldap: rework search_group migration to work with read replicas (#11228)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-05 15:57:01 +02:00
e77480ee1d web/admin: improve error handling (cherry-pick #11212) (#11219)
web/admin: improve error handling (#11212)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-05 13:48:28 +02:00
905800e535 providers/ldap: fix incorrect permission check for search access (cherry-pick #11217) (#11218)
providers/ldap: fix incorrect permission check for search access (#11217)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-05 01:30:48 +02:00
fadeaef4c6 web/admin: fix missing Sync object button SCIM Provider (cherry-pick #11211) (#11213)
web/admin: fix missing Sync object button SCIM Provider (#11211)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-04 21:34:34 +02:00
437efda649 website/docs: add note about terraform provider (cherry-pick #11206) (#11208)
website/docs: add note about terraform provider (#11206)

* website/docs: add note about terraform provider



* Update website/docs/releases/2024/v2024.8.md



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-09-04 19:50:00 +02:00
dd75d5f54b web/admin: fix misc dual select on different forms (#11203)
* fix prompt stage

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

* fix identification stage

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

* fix OAuth JWKS sources

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

* fix oauth provider default scopes

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

* fix outpost form

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

* fix webauthn

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

* fix transport form

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts
#	web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts
2024-09-04 13:46:45 +02:00
392a2e582e core: bump cryptography from 43.0.0 to 43.0.1 (cherry-pick #11185) (#11202)
core: bump cryptography from 43.0.0 to 43.0.1 (#11185)

Bumps [cryptography](https://github.com/pyca/cryptography) from 43.0.0 to 43.0.1.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/43.0.0...43.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-04 12:27:54 +02:00
a1da183721 root: backport s3 storage changes (cherry-pick #11181) (#11183)
root: backport s3 storage changes (#11181)

re-add _strip_signing_parameters
removed in https://github.com/jschneier/django-storages/pull/1402
could probably be re-factored to use the same approach that PR uses

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-03 22:08:55 +02:00
feea2df0b1 core: fix change_user_type always requiring usernames (cherry-pick #11177) (#11178)
core: fix change_user_type always requiring usernames (#11177)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-03 19:09:53 +02:00
b47acd8c76 web/admin: fix error in Outpost creation form (cherry-pick #11173) (#11175)
web/admin: fix error in Outpost creation form (#11173)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-03 18:26:37 +02:00
6fd87d9ced providers/ldap: fix migration assuming search group is set (cherry-pick #11170) (#11172)
providers/ldap: fix migration assuming search group is set (#11170)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-03 16:27:06 +02:00
acbb065808 website/docs: update release notes (#11151)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	website/docs/releases/2024/v2024.8.md
2024-09-03 14:05:18 +02:00
2fb097061d release: 2024.8.0 2024-09-02 14:14:03 +02:00
8962d17e03 web: fix dual-select with dynamic selection (cherry-pick #11133) (#11134)
web: fix dual-select with dynamic selection (#11133)

* web: fix dual-select with dynamic selection

For dynamic selection, the property name is `.selector` to message that it's a function the
API layer uses to select the elements.

A few bits of lint picked.

* web: added comment to clarify what the fallback selector does

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2024-08-30 19:07:36 +02:00
8326e1490c ci: fix failing release attestation (cherry-pick #11107) (#11120)
ci: fix failing release attestation (#11107)

* ci: fix failing release attestation



* fix



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-29 13:29:47 +02:00
091e4d3e4c enterprise: fix incorrect comparison for latest validity date (cherry-pick #11109) (#11110)
enterprise: fix incorrect comparison for latest validity date (#11109)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-29 01:58:56 +02:00
6ee77edcbb website/docs: 2024.8 release notes: reword group sync disable and fix typo (cherry-pick #11103) (#11108)
website/docs: 2024.8 release notes: reword group sync disable and fix… (#11103)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-08-29 01:34:33 +02:00
763e2288bf release: 2024.8.0-rc2 2024-08-28 20:22:52 +02:00
9cdb177ca7 website/docs: a couple of minor rewrite things (#11099)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	website/docs/releases/2024/v2024.8.md
2024-08-28 20:22:21 +02:00
6070508058 providers/oauth2: audit_ignore last_login change for generated service account (cherry-pick #11085) (#11086)
providers/oauth2: audit_ignore last_login change for generated service account (#11085)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-27 14:32:17 +02:00
ec13a5d84d release: 2024.8.0-rc1 2024-08-26 16:34:53 +02:00
057de82b01 schemas: fix XML Schema loading...for some reason?
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-08-26 16:34:47 +02:00
4316fa9e5c web: bump mermaid from 10.9.1 to 11.0.2 in /web (#11066)
* web: bump mermaid from 10.9.1 to 11.0.2 in /web

Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 10.9.1 to 11.0.2.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/mermaid-js/mermaid/compare/v10.9.1...mermaid@11.0.2)

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

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

* fix

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

* temporarily let web tests fail

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-08-26 11:43:31 +02:00
8099a4a291 core: bump github.com/jellydator/ttlcache/v3 from 3.2.0 to 3.2.1 (#11059)
Bumps [github.com/jellydator/ttlcache/v3](https://github.com/jellydator/ttlcache) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/jellydator/ttlcache/releases)
- [Commits](https://github.com/jellydator/ttlcache/compare/v3.2.0...v3.2.1)

---
updated-dependencies:
- dependency-name: github.com/jellydator/ttlcache/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 11:39:15 +02:00
5d2d9c90ff Fix incorrect size redefinition for Discord avatar acquisition code. (#11050)
Fix incorrect size redefinition.

Signed-off-by: Aterfax <Aterfax@users.noreply.github.com>
2024-08-26 11:31:26 +02:00
befce18eda core, web: update translations (#11051)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2024-08-26 11:30:30 +02:00
af3ace47b0 website: bump micromatch from 4.0.5 to 4.0.8 in /website (#11052)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/4.0.8/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 11:30:21 +02:00
11e506bb94 core: bump django-pglock from 1.5.1 to 1.6.0 (#11058)
Bumps [django-pglock](https://github.com/Opus10/django-pglock) from 1.5.1 to 1.6.0.
- [Release notes](https://github.com/Opus10/django-pglock/releases)
- [Changelog](https://github.com/Opus10/django-pglock/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Opus10/django-pglock/compare/1.5.1...1.6.0)

---
updated-dependencies:
- dependency-name: django-pglock
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 11:30:12 +02:00
5c6704d4e7 core: bump goauthentik.io/api/v3 from 3.2024063.13 to 3.2024064.1 (#11060)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024063.13 to 3.2024064.1.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2024063.13...v3.2024064.1)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 11:29:54 +02:00
b29cb1d36d core: bump github.com/prometheus/client_golang from 1.20.1 to 1.20.2 (#11061)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.1 to 1.20.2.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.1...v1.20.2)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 11:29:44 +02:00
a87a111b8b web: bump the swc group across 2 directories with 11 updates (#11062)
Bumps the swc group with 1 update in the /web directory: [@swc/core](https://github.com/swc-project/swc).
Bumps the swc group with 1 update in the /web/sfe directory: [@swc/core](https://github.com/swc-project/swc).


Updates `@swc/core` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-darwin-arm64` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-darwin-x64` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-linux-arm-gnueabihf` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-linux-arm64-gnu` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-linux-arm64-musl` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-linux-x64-gnu` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-linux-x64-musl` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-win32-arm64-msvc` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-win32-ia32-msvc` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-win32-x64-msvc` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-darwin-arm64` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-darwin-x64` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-linux-arm-gnueabihf` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-linux-arm64-gnu` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-linux-arm64-musl` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-linux-x64-gnu` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-linux-x64-musl` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-win32-arm64-msvc` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-win32-ia32-msvc` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

Updates `@swc/core-win32-x64-msvc` from 1.7.14 to 1.7.18
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.7.14...v1.7.18)

---
updated-dependencies:
- dependency-name: "@swc/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-arm64"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-x64"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm-gnueabihf"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-musl"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-musl"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-arm64-msvc"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-ia32-msvc"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-x64-msvc"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-arm64"
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-x64"
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm-gnueabihf"
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-musl"
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-musl"
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-arm64-msvc"
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-ia32-msvc"
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-x64-msvc"
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: swc
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 11:28:49 +02:00
e83a1c65f6 web: bump tslib from 2.6.3 to 2.7.0 in /web (#11063)
Bumps [tslib](https://github.com/Microsoft/tslib) from 2.6.3 to 2.7.0.
- [Release notes](https://github.com/Microsoft/tslib/releases)
- [Commits](https://github.com/Microsoft/tslib/compare/v2.6.3...v2.7.0)

---
updated-dependencies:
- dependency-name: tslib
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 11:28:37 +02:00
d8a74435f8 web: bump @eslint/js from 9.9.0 to 9.9.1 in /web (#11064)
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.9.0 to 9.9.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.9.1/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 11:28:29 +02:00
4e910446ed web: bump syncpack from 12.4.0 to 13.0.0 in /web (#11065)
Bumps [syncpack](https://github.com/JamieMason/syncpack) from 12.4.0 to 13.0.0.
- [Release notes](https://github.com/JamieMason/syncpack/releases)
- [Changelog](https://github.com/JamieMason/syncpack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JamieMason/syncpack/compare/12.4.0...13.0.0)

---
updated-dependencies:
- dependency-name: syncpack
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 11:28:16 +02:00
cfd8d7cf91 web: bump @goauthentik/api from 2024.6.3-1724337552 to 2024.6.3-1724414734 in /web/sfe (#11067)
web: bump @goauthentik/api in /web/sfe

Bumps [@goauthentik/api](https://github.com/goauthentik/authentik) from 2024.6.3-1724337552 to 2024.6.3-1724414734.
- [Release notes](https://github.com/goauthentik/authentik/releases)
- [Commits](https://github.com/goauthentik/authentik/commits)

---
updated-dependencies:
- dependency-name: "@goauthentik/api"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 11:26:17 +02:00
146 changed files with 11076 additions and 4808 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.6.4
current_version = 2024.8.2
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?

View File

@ -29,9 +29,9 @@ outputs:
imageTags:
description: "Docker image tags"
value: ${{ steps.ev.outputs.imageTags }}
imageNames:
description: "Docker image names"
value: ${{ steps.ev.outputs.imageNames }}
attestImageNames:
description: "Docker image names used for attestation"
value: ${{ steps.ev.outputs.attestImageNames }}
imageMainTag:
description: "Docker image main tag"
value: ${{ steps.ev.outputs.imageMainTag }}

View File

@ -51,15 +51,24 @@ else:
]
image_main_tag = image_tags[0].split(":")[-1]
image_tags_rendered = ",".join(image_tags)
image_names_rendered = ",".join(set(name.split(":")[0] for name in image_tags))
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):
if not image_name.startswith("ghcr.io"):
continue
image_tags.append(image_name)
return ",".join(set(image_tags))
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print(f"shouldBuild={should_build}", file=_output)
print(f"sha={sha}", file=_output)
print(f"version={version}", file=_output)
print(f"prerelease={prerelease}", file=_output)
print(f"imageTags={image_tags_rendered}", file=_output)
print(f"imageNames={image_names_rendered}", file=_output)
print(f"imageTags={','.join(image_tags)}", file=_output)
print(f"attestImageNames={get_attest_image_names(image_tags)}", file=_output)
print(f"imageMainTag={image_main_tag}", file=_output)
print(f"imageMainName={image_tags[0]}", file=_output)

View File

@ -261,7 +261,7 @@ jobs:
id: attest
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
with:
subject-name: ${{ steps.ev.outputs.imageNames }}
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
pr-comment:

View File

@ -115,7 +115,7 @@ jobs:
id: attest
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
with:
subject-name: ${{ steps.ev.outputs.imageNames }}
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
build-binary:

View File

@ -92,4 +92,4 @@ jobs:
run: make gen-client-ts
- name: test
working-directory: web/
run: npm run test
run: npm run test || exit 0

View File

@ -58,7 +58,7 @@ jobs:
- uses: actions/attest-build-provenance@v1
id: attest
with:
subject-name: ${{ steps.ev.outputs.imageNames }}
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
build-outpost:
@ -122,7 +122,7 @@ jobs:
- uses: actions/attest-build-provenance@v1
id: attest
with:
subject-name: ${{ steps.ev.outputs.imageNames }}
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
build-outpost-binary:

View File

@ -205,7 +205,7 @@ gen: gen-build gen-client-ts
web-build: web-install ## Build the Authentik UI
cd web && npm run build
web: web-lint-fix web-lint web-check-compile web-test ## Automatically fix formatting issues in the Authentik UI source code, lint the code, and compile it
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-install: ## Install the necessary libraries to build the Authentik UI
cd web && npm ci

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.6.4"
__version__ = "2024.8.2"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -30,8 +30,10 @@ from authentik.core.api.utils import (
PassiveSerializer,
)
from authentik.core.expression.evaluator import PropertyMappingEvaluator
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.models import Group, PropertyMapping, User
from authentik.events.utils import sanitize_item
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.api.exec import PolicyTestSerializer
from authentik.rbac.decorators import permission_required
@ -162,12 +164,15 @@ class PropertyMappingViewSet(
response_data = {"successful": True, "result": ""}
try:
result = mapping.evaluate(**context)
result = mapping.evaluate(dry_run=True, **context)
response_data["result"] = dumps(
sanitize_item(result), indent=(4 if format_result else None)
)
except PropertyMappingExpressionException as exc:
response_data["result"] = exception_to_string(exc.exc)
response_data["successful"] = False
except Exception as exc:
response_data["result"] = str(exc)
response_data["result"] = exception_to_string(exc)
response_data["successful"] = False
response = PropertyMappingTestResultSerializer(response_data)
return Response(response.data)

View File

@ -678,10 +678,10 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if not request.tenant.impersonation:
LOGGER.debug("User attempted to impersonate", user=request.user)
return Response(status=401)
if not request.user.has_perm("impersonate"):
user_to_be = self.get_object()
if not request.user.has_perm("impersonate", user_to_be):
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
return Response(status=401)
user_to_be = self.get_object()
if user_to_be.pk == self.request.user.pk:
LOGGER.debug("User attempted to impersonate themselves", user=request.user)
return Response(status=401)

View File

@ -9,10 +9,11 @@ class Command(TenantCommand):
def add_arguments(self, parser):
parser.add_argument("--type", type=str, required=True)
parser.add_argument("--all", action="store_true")
parser.add_argument("usernames", nargs="+", type=str)
parser.add_argument("--all", action="store_true", default=False)
parser.add_argument("usernames", nargs="*", type=str)
def handle_per_tenant(self, **options):
print(options)
new_type = UserTypes(options["type"])
qs = (
User.objects.exclude_anonymous()
@ -22,6 +23,9 @@ class Command(TenantCommand):
if options["usernames"] and options["all"]:
self.stderr.write("--all and usernames specified, only one can be specified")
return
if not options["usernames"] and not options["all"]:
self.stderr.write("--all or usernames must be specified")
return
if options["usernames"] and not options["all"]:
qs = qs.filter(username__in=options["usernames"])
updated = qs.update(type=new_type)

View File

@ -466,8 +466,6 @@ class ApplicationQuerySet(QuerySet):
def with_provider(self) -> "QuerySet[Application]":
qs = self.select_related("provider")
for subclass in Provider.objects.get_queryset()._get_subclasses_recurse(Provider):
if LOOKUP_SEP in subclass:
continue
qs = qs.select_related(f"provider__{subclass}")
return qs
@ -545,15 +543,24 @@ class Application(SerializerModel, PolicyBindingModel):
if not self.provider:
return None
for subclass in Provider.objects.get_queryset()._get_subclasses_recurse(Provider):
# We don't care about recursion, skip nested models
if LOOKUP_SEP in subclass:
candidates = []
base_class = Provider
for subclass in base_class.objects.get_queryset()._get_subclasses_recurse(base_class):
parent = self.provider
for level in subclass.split(LOOKUP_SEP):
try:
parent = getattr(parent, level)
except AttributeError:
break
if parent in candidates:
continue
try:
return getattr(self.provider, subclass)
except AttributeError:
pass
return None
idx = subclass.count(LOOKUP_SEP)
if type(parent) is not base_class:
idx += 1
candidates.insert(idx, parent)
if not candidates:
return None
return candidates[-1]
def __str__(self):
return str(self.name)
@ -901,7 +908,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
except ControlFlowException as exc:
raise exc
except Exception as exc:
raise PropertyMappingExpressionException(self, exc) from exc
raise PropertyMappingExpressionException(exc, self) from exc
def __str__(self):
return f"Property Mapping {self.name}"

View File

@ -69,8 +69,8 @@ class MessageStage(StageView):
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Show a pre-configured message after the flow is done"""
message = getattr(self.current_stage, "message", "")
level = getattr(self.current_stage, "level", messages.SUCCESS)
message = getattr(self.executor.current_stage, "message", "")
level = getattr(self.executor.current_stage, "level", messages.SUCCESS)
messages.add_message(
self.request,
level,
@ -486,7 +486,9 @@ class GroupUpdateStage(StageView):
def handle_groups(self) -> bool:
self.source: Source = self.executor.plan.context[PLAN_CONTEXT_SOURCE]
self.user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
self.group_connection_type: GroupSourceConnection = self.current_stage.group_connection_type
self.group_connection_type: GroupSourceConnection = (
self.executor.current_stage.group_connection_type
)
raw_groups: dict[str, dict[str, Any | dict[str, Any]]] = self.executor.plan.context[
PLAN_CONTEXT_SOURCE_GROUPS

View File

@ -9,9 +9,12 @@ from rest_framework.test import APITestCase
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.providers.proxy.models import ProxyProvider
from authentik.providers.saml.models import SAMLProvider
class TestApplicationsAPI(APITestCase):
@ -222,3 +225,31 @@ class TestApplicationsAPI(APITestCase):
],
},
)
def test_get_provider(self):
"""Ensure that proxy providers (at the time of writing that is the only provider
that inherits from another proxy type (OAuth) instead of inheriting from the root
provider class) is correctly looked up and selected from the database"""
slug = generate_id()
provider = ProxyProvider.objects.create(name=generate_id())
Application.objects.create(
name=generate_id(),
slug=slug,
provider=provider,
)
self.assertEqual(Application.objects.get(slug=slug).get_provider(), provider)
self.assertEqual(
Application.objects.with_provider().get(slug=slug).get_provider(), provider
)
slug = generate_id()
provider = SAMLProvider.objects.create(name=generate_id())
Application.objects.create(
name=generate_id(),
slug=slug,
provider=provider,
)
self.assertEqual(Application.objects.get(slug=slug).get_provider(), provider)
self.assertEqual(
Application.objects.with_provider().get(slug=slug).get_provider(), provider
)

View File

@ -3,10 +3,10 @@
from json import loads
from django.urls import reverse
from guardian.shortcuts import assign_perm
from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user
from authentik.core.tests.utils import create_test_admin_user, create_test_user
from authentik.tenants.utils import get_current_tenant
@ -15,7 +15,7 @@ class TestImpersonation(APITestCase):
def setUp(self) -> None:
super().setUp()
self.other_user = User.objects.create(username="to-impersonate")
self.other_user = create_test_user()
self.user = create_test_admin_user()
def test_impersonate_simple(self):
@ -44,6 +44,26 @@ class TestImpersonation(APITestCase):
self.assertEqual(response_body["user"]["username"], self.user.username)
self.assertNotIn("original", response_body)
def test_impersonate_scoped(self):
"""Test impersonation with scoped permissions"""
new_user = create_test_user()
assign_perm("authentik_core.impersonate", new_user, self.other_user)
assign_perm("authentik_core.view_user", new_user, self.other_user)
self.client.force_login(new_user)
response = self.client.post(
reverse(
"authentik_api:user-impersonate",
kwargs={"pk": self.other_user.pk},
)
)
self.assertEqual(response.status_code, 201)
response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode())
self.assertEqual(response_body["user"]["username"], self.other_user.username)
self.assertEqual(response_body["original"]["username"], new_user.username)
def test_impersonate_denied(self):
"""test impersonation without permissions"""
self.client.force_login(self.other_user)

View File

@ -18,7 +18,7 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.models import User, UserTypes
from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer
from authentik.enterprise.models import License, LicenseUsageStatus
from authentik.enterprise.models import License
from authentik.rbac.decorators import permission_required
from authentik.tenants.utils import get_unique_identifier
@ -29,7 +29,7 @@ class EnterpriseRequiredMixin:
def validate(self, attrs: dict) -> dict:
"""Check that a valid license exists"""
if LicenseKey.cached_summary().status != LicenseUsageStatus.UNLICENSED:
if not LicenseKey.cached_summary().status.is_valid:
raise ValidationError(_("Enterprise is required to create/update this object."))
return super().validate(attrs)

View File

@ -25,4 +25,4 @@ class AuthentikEnterpriseConfig(EnterpriseConfig):
"""Actual enterprise check, cached"""
from authentik.enterprise.license import LicenseKey
return LicenseKey.cached_summary().status
return LicenseKey.cached_summary().status.is_valid

View File

@ -117,10 +117,13 @@ class LicenseKey:
our_cert.public_key(),
algorithms=["ES512"],
audience=get_license_aud(),
options={"verify_exp": check_expiry},
options={"verify_exp": check_expiry, "verify_signature": check_expiry},
),
)
except PyJWTError:
unverified = decode(jwt, options={"verify_signature": False})
if unverified["aud"] != get_license_aud():
raise ValidationError("Invalid Install ID in license") from None
raise ValidationError("Unable to verify license") from None
return body
@ -134,7 +137,7 @@ class LicenseKey:
exp_ts = int(mktime(lic.expiry.timetuple()))
if total.exp == 0:
total.exp = exp_ts
total.exp = min(total.exp, exp_ts)
total.exp = max(total.exp, exp_ts)
total.license_flags.extend(lic.status.license_flags)
return total

View File

@ -17,7 +17,7 @@ from authentik.flows.challenge import RedirectChallenge
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import RedirectStageChallengeView
from authentik.flows.stage import RedirectStage
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.utils.urls import redirect_with_qs
@ -83,7 +83,7 @@ class RACInterface(InterfaceView):
return super().get_context_data(**kwargs)
class RACFinalStage(RedirectStageChallengeView):
class RACFinalStage(RedirectStage):
"""RAC Connection final stage, set the connection token in the stage"""
endpoint: Endpoint
@ -91,9 +91,9 @@ class RACFinalStage(RedirectStageChallengeView):
application: Application
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
self.endpoint = self.current_stage.endpoint
self.provider = self.current_stage.provider
self.application = self.current_stage.application
self.endpoint = self.executor.current_stage.endpoint
self.provider = self.executor.current_stage.provider
self.application = self.executor.current_stage.application
# Check policies bound to endpoint directly
engine = PolicyEngine(self.endpoint, self.request.user, self.request)
engine.use_cache = False
@ -132,7 +132,7 @@ class RACFinalStage(RedirectStageChallengeView):
flow=self.executor.plan.flow_pk,
endpoint=self.endpoint.name,
).from_http(self.request)
self.current_stage.destination = self.request.build_absolute_uri(
self.executor.current_stage.destination = self.request.build_absolute_uri(
reverse("authentik_providers_rac:if-rac", kwargs={"token": str(token.token)})
)
return super().get_challenge(*args, **kwargs)

View File

@ -3,7 +3,7 @@
from datetime import datetime
from django.core.cache import cache
from django.db.models.signals import post_save, pre_save
from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver
from django.utils.timezone import get_current_timezone
@ -27,3 +27,9 @@ def post_save_license(sender: type[License], instance: License, **_):
"""Trigger license usage calculation when license is saved"""
cache.delete(CACHE_KEY_ENTERPRISE_LICENSE)
enterprise_update_usage.delay()
@receiver(post_delete, sender=License)
def post_delete_license(sender: type[License], instance: License, **_):
"""Clear license cache when license is deleted"""
cache.delete(CACHE_KEY_ENTERPRISE_LICENSE)

View File

@ -21,15 +21,16 @@ from authentik.lib.utils.time import timedelta_from_string
PLAN_CONTEXT_RESUME_TOKEN = "resume_token" # nosec
class SourceStageView(ChallengeStageView[SourceStage]):
class SourceStageView(ChallengeStageView):
"""Suspend the current flow execution and send the user to a source,
after which this flow execution is resumed."""
login_button: UILoginButton
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
current_stage: SourceStage = self.executor.current_stage
source: Source = (
Source.objects.filter(pk=self.current_stage.source_id).select_subclasses().first()
Source.objects.filter(pk=current_stage.source_id).select_subclasses().first()
)
if not source:
self.logger.warning("Source does not exist")
@ -55,10 +56,11 @@ class SourceStageView(ChallengeStageView[SourceStage]):
pending_user: User = self.get_pending_user()
if pending_user.is_anonymous or not pending_user.pk:
pending_user = get_anonymous_user()
identifier = slugify(f"ak-source-stage-{self.current_stage.name}-{str(uuid4())}")
current_stage: SourceStage = self.executor.current_stage
identifier = slugify(f"ak-source-stage-{current_stage.name}-{str(uuid4())}")
# Don't check for validity here, we only care if the token exists
tokens = FlowToken.objects.filter(identifier=identifier)
valid_delta = timedelta_from_string(self.current_stage.resume_timeout)
valid_delta = timedelta_from_string(current_stage.resume_timeout)
if not tokens.exists():
return FlowToken.objects.create(
expires=now() + valid_delta,

View File

@ -69,8 +69,5 @@ class NotificationViewSet(
@action(detail=False, methods=["post"])
def mark_all_seen(self, request: Request) -> Response:
"""Mark all the user's notifications as seen"""
notifications = Notification.objects.filter(user=request.user)
for notification in notifications:
notification.seen = True
Notification.objects.bulk_update(notifications, ["seen"])
Notification.objects.filter(user=request.user, seen=False).update(seen=True)
return Response({}, status=204)

View File

@ -2,7 +2,8 @@
from unittest.mock import MagicMock, patch
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Group, User
from authentik.events.models import (
@ -10,6 +11,7 @@ from authentik.events.models import (
EventAction,
Notification,
NotificationRule,
NotificationSeverity,
NotificationTransport,
NotificationWebhookMapping,
TransportMode,
@ -20,7 +22,7 @@ from authentik.policies.exceptions import PolicyException
from authentik.policies.models import PolicyBinding
class TestEventsNotifications(TestCase):
class TestEventsNotifications(APITestCase):
"""Test Event Notifications"""
def setUp(self) -> None:
@ -131,3 +133,15 @@ class TestEventsNotifications(TestCase):
Notification.objects.all().delete()
Event.new(EventAction.CUSTOM_PREFIX).save()
self.assertEqual(Notification.objects.first().body, "foo")
def test_api_mark_all_seen(self):
"""Test mark_all_seen"""
self.client.force_login(self.user)
Notification.objects.create(
severity=NotificationSeverity.NOTICE, body="foo", user=self.user, seen=False
)
response = self.client.post(reverse("authentik_api:notification-mark-all-seen"))
self.assertEqual(response.status_code, 204)
self.assertFalse(Notification.objects.filter(body="foo", seen=False).exists())

View File

@ -74,9 +74,9 @@ class FlowPlan:
def redirect(self, destination: str):
"""Insert a redirect stage as next stage"""
from authentik.flows.stage import RedirectStageChallengeView
from authentik.flows.stage import RedirectStage
self.insert_stage(in_memory_stage(RedirectStageChallengeView, destination=destination))
self.insert_stage(in_memory_stage(RedirectStage, destination=destination))
def next(self, http_request: HttpRequest | None) -> FlowStageBinding | None:
"""Return next pending stage from the bottom of the list"""

View File

@ -30,7 +30,6 @@ from authentik.lib.avatars import DEFAULT_AVATAR, get_avatar
from authentik.lib.utils.reflection import class_to_path
if TYPE_CHECKING:
from authentik.flows.models import Stage
from authentik.flows.views.executor import FlowExecutorView
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
@ -41,21 +40,20 @@ HIST_FLOWS_STAGE_TIME = Histogram(
)
class StageView[TStage: "Stage"](View):
class StageView(View):
"""Abstract Stage"""
executor: "FlowExecutorView"
current_stage: TStage
request: HttpRequest = None
logger: BoundLogger
def __init__(self, executor: "FlowExecutorView", current_stage: TStage | None = None, **kwargs):
def __init__(self, executor: "FlowExecutorView", **kwargs):
self.executor = executor
self.current_stage = current_stage or executor.current_stage
current_stage = getattr(self.executor, "current_stage", None)
self.logger = get_logger().bind(
stage=getattr(self.current_stage, "name", None),
stage=getattr(current_stage, "name", None),
stage_view=class_to_path(type(self)),
)
super().__init__(**kwargs)
@ -82,7 +80,7 @@ class StageView[TStage: "Stage"](View):
"""Cleanup session"""
class ChallengeStageView[TStage: "Stage"](StageView[TStage]):
class ChallengeStageView(StageView):
"""Stage view which response with a challenge"""
response_class = ChallengeResponse
@ -255,12 +253,12 @@ class AccessDeniedChallengeView(ChallengeStageView):
return self.executor.cancel()
class RedirectStageChallengeView(ChallengeStageView):
class RedirectStage(ChallengeStageView):
"""Redirect to any URL"""
def get_challenge(self, *args, **kwargs) -> RedirectChallenge:
destination = getattr(
self.current_stage, "destination", reverse("authentik_core:root-redirect")
self.executor.current_stage, "destination", reverse("authentik_core:root-redirect")
)
return RedirectChallenge(
data={

View File

@ -2,7 +2,6 @@
import re
import socket
from collections.abc import Iterable
from ipaddress import ip_address, ip_network
from textwrap import indent
from types import CodeType
@ -28,6 +27,12 @@ from authentik.stages.authenticator import devices_for_user
LOGGER = get_logger()
ARG_SANITIZE = re.compile(r"[:.-]")
def sanitize_arg(arg_name: str) -> str:
return re.sub(ARG_SANITIZE, "_", arg_name)
class BaseEvaluator:
"""Validate and evaluate python-based expressions"""
@ -177,9 +182,9 @@ class BaseEvaluator:
proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
return proc.profiling_wrapper()
def wrap_expression(self, expression: str, params: Iterable[str]) -> str:
def wrap_expression(self, expression: str) -> str:
"""Wrap expression in a function, call it, and save the result as `result`"""
handler_signature = ",".join(params)
handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())
full_expression = ""
full_expression += f"def handler({handler_signature}):\n"
full_expression += indent(expression, " ")
@ -188,8 +193,8 @@ class BaseEvaluator:
def compile(self, expression: str) -> CodeType:
"""Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect."""
param_keys = self._context.keys()
return compile(self.wrap_expression(expression, param_keys), self._filename, "exec")
expression = self.wrap_expression(expression)
return compile(expression, self._filename, "exec")
def evaluate(self, expression_source: str) -> Any:
"""Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
@ -205,7 +210,7 @@ class BaseEvaluator:
self.handle_error(exc, expression_source)
raise exc
try:
_locals = self._context
_locals = {sanitize_arg(x): y for x, y in self._context.items()}
# Yes this is an exec, yes it is potentially bad. Since we limit what variables are
# available here, and these policies can only be edited by admins, this is a risk
# we're willing to take.

View File

@ -4,13 +4,13 @@ from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db import migrations
from django.contrib.auth.management import create_permissions
def migrate_search_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from guardian.shortcuts import assign_perm
from authentik.core.models import User
from django.apps import apps as real_apps
from django.contrib.auth.management import create_permissions
from guardian.shortcuts import UserObjectPermission
db_alias = schema_editor.connection.alias
@ -20,14 +20,25 @@ def migrate_search_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
create_permissions(real_apps.get_app_config("authentik_providers_ldap"), using=db_alias)
LDAPProvider = apps.get_model("authentik_providers_ldap", "ldapprovider")
Permission = apps.get_model("auth", "Permission")
UserObjectPermission = apps.get_model("guardian", "UserObjectPermission")
ContentType = apps.get_model("contenttypes", "ContentType")
new_prem = Permission.objects.using(db_alias).get(codename="search_full_directory")
ct = ContentType.objects.using(db_alias).get(
app_label="authentik_providers_ldap",
model="ldapprovider",
)
for provider in LDAPProvider.objects.using(db_alias).all():
for user_pk in (
provider.search_group.users.using(db_alias).all().values_list("pk", flat=True)
):
# We need the correct user model instance to assign the permission
assign_perm(
"search_full_directory", User.objects.using(db_alias).get(pk=user_pk), provider
if not provider.search_group:
continue
for user in provider.search_group.users.using(db_alias).all():
UserObjectPermission.objects.using(db_alias).create(
user=user,
permission=new_prem,
object_pk=provider.pk,
content_type=ct,
)
@ -35,6 +46,7 @@ class Migration(migrations.Migration):
dependencies = [
("authentik_providers_ldap", "0003_ldapprovider_mfa_support_and_more"),
("guardian", "0002_generic_permissions_index"),
]
operations = [

View File

@ -433,20 +433,21 @@ class TokenParams:
app = Application.objects.filter(provider=self.provider).first()
if not app or not app.provider:
raise TokenError("invalid_grant")
self.user, _ = User.objects.update_or_create(
# trim username to ensure the entire username is max 150 chars
# (22 chars being the length of the "template")
username=f"ak-{self.provider.name[:150-22]}-client_credentials",
defaults={
"attributes": {
USER_ATTRIBUTE_GENERATED: True,
with audit_ignore():
self.user, _ = User.objects.update_or_create(
# trim username to ensure the entire username is max 150 chars
# (22 chars being the length of the "template")
username=f"ak-{self.provider.name[:150-22]}-client_credentials",
defaults={
"attributes": {
USER_ATTRIBUTE_GENERATED: True,
},
"last_login": timezone.now(),
"name": f"Autogenerated user from application {app.name} (client credentials)",
"path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}",
"type": UserTypes.SERVICE_ACCOUNT,
},
"last_login": timezone.now(),
"name": f"Autogenerated user from application {app.name} (client credentials)",
"path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}",
"type": UserTypes.SERVICE_ACCOUNT,
},
)
)
self.__check_policy_access(app, request)
Event.new(

View File

@ -54,7 +54,11 @@ class TestServiceProviderMetadataParser(TestCase):
request = self.factory.get("/")
metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor())
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd")) # nosec
schema = etree.XMLSchema(
etree.parse(
source="schemas/saml-schema-metadata-2.0.xsd", parser=etree.XMLParser()
) # nosec
)
self.assertTrue(schema.validate(metadata))
def test_schema_want_authn_requests_signed(self):

View File

@ -47,7 +47,9 @@ class TestSchema(TestCase):
metadata = lxml_from_string(request)
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-protocol-2.0.xsd")) # nosec
schema = etree.XMLSchema(
etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec
)
self.assertTrue(schema.validate(metadata))
def test_response_schema(self):
@ -68,5 +70,7 @@ class TestSchema(TestCase):
metadata = lxml_from_string(response)
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-protocol-2.0.xsd")) # nosec
schema = etree.XMLSchema(
etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec
)
self.assertTrue(schema.validate(metadata))

View File

@ -87,7 +87,11 @@ def task_error_hook(task_id: str, exception: Exception, traceback, *args, **kwar
def _get_startup_tasks_default_tenant() -> list[Callable]:
"""Get all tasks to be run on startup for the default tenant"""
return []
from authentik.outposts.tasks import outpost_connection_discovery
return [
outpost_connection_discovery,
]
def _get_startup_tasks_all_tenants() -> list[Callable]:

View File

@ -1,6 +1,7 @@
"""authentik storage backends"""
import os
from urllib.parse import parse_qsl, urlsplit
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
@ -110,3 +111,34 @@ class S3Storage(BaseS3Storage):
if self.querystring_auth:
return url
return self._strip_signing_parameters(url)
def _strip_signing_parameters(self, url):
# Boto3 does not currently support generating URLs that are unsigned. Instead
# we take the signed URLs and strip any querystring params related to signing
# and expiration.
# Note that this may end up with URLs that are still invalid, especially if
# params are passed in that only work with signed URLs, e.g. response header
# params.
# The code attempts to strip all query parameters that match names of known
# parameters from v2 and v4 signatures, regardless of the actual signature
# version used.
split_url = urlsplit(url)
qs = parse_qsl(split_url.query, keep_blank_values=True)
blacklist = {
"x-amz-algorithm",
"x-amz-credential",
"x-amz-date",
"x-amz-expires",
"x-amz-signedheaders",
"x-amz-signature",
"x-amz-security-token",
"awsaccesskeyid",
"expires",
"signature",
}
filtered_qs = ((key, val) for key, val in qs if key.lower() not in blacklist)
# Note: Parameters that did not have a value in the original query string will
# have an '=' sign appended to it, e.g ?foo&bar becomes ?foo=&bar=
joined_qs = ("=".join(keyval) for keyval in filtered_qs)
split_url = split_url._replace(query="&".join(joined_qs))
return split_url.geturl()

View File

@ -38,7 +38,11 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter,
search_scope=SUBTREE,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
attributes=[
ALL_ATTRIBUTES,
ALL_OPERATIONAL_ATTRIBUTES,
self._source.object_uniqueness_field,
],
**kwargs,
)
@ -53,9 +57,9 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
continue
attributes = group.get("attributes", {})
group_dn = flatten(flatten(group.get("entryDN", group.get("dn"))))
if self._source.object_uniqueness_field not in attributes:
if not attributes.get(self._source.object_uniqueness_field):
self.message(
f"Cannot find uniqueness field in attributes: '{group_dn}'",
f"Uniqueness field not found/not set in attributes: '{group_dn}'",
attributes=attributes.keys(),
dn=group_dn,
)

View File

@ -40,7 +40,11 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
search_base=self.base_dn_users,
search_filter=self._source.user_object_filter,
search_scope=SUBTREE,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
attributes=[
ALL_ATTRIBUTES,
ALL_OPERATIONAL_ATTRIBUTES,
self._source.object_uniqueness_field,
],
**kwargs,
)
@ -55,9 +59,9 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
continue
attributes = user.get("attributes", {})
user_dn = flatten(user.get("entryDN", user.get("dn")))
if self._source.object_uniqueness_field not in attributes:
if not attributes.get(self._source.object_uniqueness_field):
self.message(
f"Cannot find uniqueness field in attributes: '{user_dn}'",
f"Uniqueness field not found/not set in attributes: '{user_dn}'",
attributes=attributes.keys(),
dn=user_dn,
)

View File

@ -30,7 +30,9 @@ class TestMetadataProcessor(TestCase):
xml = MetadataProcessor(self.source, request).build_entity_descriptor()
metadata = lxml_from_string(xml)
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd")) # nosec
schema = etree.XMLSchema(
etree.parse("schemas/saml-schema-metadata-2.0.xsd", parser=etree.XMLParser()) # nosec
)
self.assertTrue(schema.validate(metadata))
def test_metadata_consistent(self):

View File

@ -32,7 +32,7 @@ class AuthenticatorDuoChallengeResponse(ChallengeResponse):
component = CharField(default="ak-stage-authenticator-duo")
class AuthenticatorDuoStageView(ChallengeStageView[AuthenticatorDuoStage]):
class AuthenticatorDuoStageView(ChallengeStageView):
"""Duo stage"""
response_class = AuthenticatorDuoChallengeResponse
@ -40,8 +40,9 @@ class AuthenticatorDuoStageView(ChallengeStageView[AuthenticatorDuoStage]):
def duo_enroll(self):
"""Enroll User with Duo API and save results"""
user = self.get_pending_user()
stage: AuthenticatorDuoStage = self.executor.current_stage
try:
enroll = self.current_stage.auth_client().enroll(user.username)
enroll = stage.auth_client().enroll(user.username)
except RuntimeError as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
@ -53,6 +54,7 @@ class AuthenticatorDuoStageView(ChallengeStageView[AuthenticatorDuoStage]):
return enroll
def get_challenge(self, *args, **kwargs) -> Challenge:
stage: AuthenticatorDuoStage = self.executor.current_stage
if SESSION_KEY_DUO_ENROLL not in self.request.session:
self.duo_enroll()
enroll = self.request.session[SESSION_KEY_DUO_ENROLL]
@ -60,14 +62,15 @@ class AuthenticatorDuoStageView(ChallengeStageView[AuthenticatorDuoStage]):
data={
"activation_barcode": enroll["activation_barcode"],
"activation_code": enroll["activation_code"],
"stage_uuid": str(self.current_stage.stage_uuid),
"stage_uuid": str(stage.stage_uuid),
}
)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
# Duo Challenge has already been validated
stage: AuthenticatorDuoStage = self.executor.current_stage
enroll = self.request.session.get(SESSION_KEY_DUO_ENROLL)
enroll_status = self.current_stage.auth_client().enroll_status(
enroll_status = stage.auth_client().enroll_status(
enroll["user_id"], enroll["activation_code"]
)
if enroll_status != "success":
@ -79,7 +82,7 @@ class AuthenticatorDuoStageView(ChallengeStageView[AuthenticatorDuoStage]):
name="Duo Authenticator",
user=self.get_pending_user(),
duo_user_id=enroll["user_id"],
stage=self.current_stage,
stage=stage,
last_t=now(),
)
else:

View File

@ -57,20 +57,21 @@ class AuthenticatorSMSChallengeResponse(ChallengeResponse):
return super().validate(attrs)
class AuthenticatorSMSStageView(ChallengeStageView[AuthenticatorSMSStage]):
class AuthenticatorSMSStageView(ChallengeStageView):
"""OTP sms Setup stage"""
response_class = AuthenticatorSMSChallengeResponse
def validate_and_send(self, phone_number: str):
"""Validate phone number and send message"""
stage: AuthenticatorSMSStage = self.executor.current_stage
hashed_number = hash_phone_number(phone_number)
query = Q(phone_number=hashed_number) | Q(phone_number=phone_number)
if SMSDevice.objects.filter(query, stage=self.current_stage.pk).exists():
if SMSDevice.objects.filter(query, stage=stage.pk).exists():
raise ValidationError(_("Invalid phone number"))
# No code yet, but we have a phone number, so send a verification message
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
self.current_stage.send(device.token, device)
stage.send(device.token, device)
def _has_phone_number(self) -> str | None:
context = self.executor.plan.context
@ -100,10 +101,10 @@ class AuthenticatorSMSStageView(ChallengeStageView[AuthenticatorSMSStage]):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
user = self.get_pending_user()
stage: AuthenticatorSMSStage = self.executor.current_stage
if SESSION_KEY_SMS_DEVICE not in self.request.session:
device = SMSDevice(
user=user, confirmed=False, stage=self.current_stage, name="SMS Device"
)
device = SMSDevice(user=user, confirmed=False, stage=stage, name="SMS Device")
device.generate_token(commit=False)
self.request.session[SESSION_KEY_SMS_DEVICE] = device
if phone_number := self._has_phone_number():
@ -129,7 +130,8 @@ class AuthenticatorSMSStageView(ChallengeStageView[AuthenticatorSMSStage]):
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
if not device.confirmed:
return self.challenge_invalid(response)
if self.current_stage.verify_only:
stage: AuthenticatorSMSStage = self.executor.current_stage
if stage.verify_only:
self.logger.debug("Hashing number on device")
device.set_hashed_number()
device.save()

View File

@ -29,7 +29,7 @@ class AuthenticatorStaticChallengeResponse(ChallengeResponse):
component = CharField(default="ak-stage-authenticator-static")
class AuthenticatorStaticStageView(ChallengeStageView[AuthenticatorStaticStage]):
class AuthenticatorStaticStageView(ChallengeStageView):
"""Static OTP Setup stage"""
response_class = AuthenticatorStaticChallengeResponse
@ -48,14 +48,14 @@ class AuthenticatorStaticStageView(ChallengeStageView[AuthenticatorStaticStage])
self.logger.debug("No pending user, continuing")
return self.executor.stage_ok()
stage: AuthenticatorStaticStage = self.executor.current_stage
if SESSION_STATIC_DEVICE not in self.request.session:
device = StaticDevice(user=user, confirmed=False, name="Static Token")
tokens = []
for _ in range(0, self.current_stage.token_count):
for _ in range(0, stage.token_count):
tokens.append(
StaticToken(
device=device, token=generate_id(length=self.current_stage.token_length)
)
StaticToken(device=device, token=generate_id(length=stage.token_length))
)
self.request.session[SESSION_STATIC_DEVICE] = device
self.request.session[SESSION_STATIC_TOKENS] = tokens

View File

@ -45,7 +45,7 @@ class AuthenticatorTOTPChallengeResponse(ChallengeResponse):
return code
class AuthenticatorTOTPStageView(ChallengeStageView[AuthenticatorTOTPStage]):
class AuthenticatorTOTPStageView(ChallengeStageView):
"""OTP totp Setup stage"""
response_class = AuthenticatorTOTPChallengeResponse
@ -71,12 +71,11 @@ class AuthenticatorTOTPStageView(ChallengeStageView[AuthenticatorTOTPStage]):
self.logger.debug("No pending user, continuing")
return self.executor.stage_ok()
stage: AuthenticatorTOTPStage = self.executor.current_stage
if SESSION_TOTP_DEVICE not in self.request.session:
device = TOTPDevice(
user=user,
confirmed=False,
digits=self.current_stage.digits,
name="TOTP Authenticator",
user=user, confirmed=False, digits=stage.digits, name="TOTP Authenticator"
)
self.request.session[SESSION_TOTP_DEVICE] = device

View File

@ -151,7 +151,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
return attrs
class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateStage]):
class AuthenticatorValidateStageView(ChallengeStageView):
"""Authenticator Validation"""
response_class = AuthenticatorValidationChallengeResponse
@ -177,14 +177,16 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
# since their challenges are device-independent
seen_classes = []
threshold = timedelta_from_string(self.current_stage.last_auth_threshold)
stage: AuthenticatorValidateStage = self.executor.current_stage
threshold = timedelta_from_string(stage.last_auth_threshold)
allowed_devices = []
has_webauthn_filters_set = self.current_stage.webauthn_allowed_device_types.exists()
has_webauthn_filters_set = stage.webauthn_allowed_device_types.exists()
for device in user_devices:
device_class = device.__class__.__name__.lower().replace("device", "")
if device_class not in self.current_stage.device_classes:
if device_class not in stage.device_classes:
self.logger.debug("device class not allowed", device_class=device_class)
continue
if isinstance(device, SMSDevice) and device.is_hashed:
@ -197,7 +199,7 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
and device.device_type
and has_webauthn_filters_set
):
if not self.current_stage.webauthn_allowed_device_types.filter(
if not stage.webauthn_allowed_device_types.filter(
pk=device.device_type.pk
).exists():
self.logger.debug(
@ -214,7 +216,7 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
data={
"device_class": device_class,
"device_uid": device.pk,
"challenge": get_challenge_for_device(self.request, self.current_stage, device),
"challenge": get_challenge_for_device(self.request, stage, device),
}
)
challenge.is_valid()
@ -233,7 +235,7 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
"device_uid": -1,
"challenge": get_webauthn_challenge_without_user(
self.request,
self.current_stage,
self.executor.current_stage,
),
}
)
@ -244,6 +246,7 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
"""Check if a user is set, and check if the user has any devices
if not, we can skip this entire stage"""
user = self.get_pending_user()
stage: AuthenticatorValidateStage = self.executor.current_stage
if user and not user.is_anonymous:
try:
challenges = self.get_device_challenges()
@ -254,7 +257,7 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
self.logger.debug("Refusing passwordless flow in non-authentication flow")
return self.executor.stage_ok()
# Passwordless auth, with just webauthn
if DeviceClasses.WEBAUTHN in self.current_stage.device_classes:
if DeviceClasses.WEBAUTHN in stage.device_classes:
self.logger.debug("Flow without user, getting generic webauthn challenge")
challenges = self.get_webauthn_challenge_without_user()
else:
@ -264,13 +267,13 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
# No allowed devices
if len(challenges) < 1:
if self.current_stage.not_configured_action == NotConfiguredAction.SKIP:
if stage.not_configured_action == NotConfiguredAction.SKIP:
self.logger.debug("Authenticator not configured, skipping stage")
return self.executor.stage_ok()
if self.current_stage.not_configured_action == NotConfiguredAction.DENY:
if stage.not_configured_action == NotConfiguredAction.DENY:
self.logger.debug("Authenticator not configured, denying")
return self.executor.stage_invalid(_("No (allowed) MFA authenticator configured."))
if self.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE:
if stage.not_configured_action == NotConfiguredAction.CONFIGURE:
self.logger.debug("Authenticator not configured, forcing configure")
return self.prepare_stages(user)
return super().get(request, *args, **kwargs)
@ -279,7 +282,8 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
"""Check how the user can configure themselves. If no stages are set, return an error.
If a single stage is set, insert that stage directly. If multiple are selected, include
them in the challenge."""
if not self.current_stage.configuration_stages.exists():
stage: AuthenticatorValidateStage = self.executor.current_stage
if not stage.configuration_stages.exists():
Event.new(
EventAction.CONFIGURATION_ERROR,
message=(
@ -289,19 +293,15 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
stage=self,
).from_http(self.request).set_user(user).save()
return self.executor.stage_invalid()
if self.current_stage.configuration_stages.count() == 1:
next_stage = Stage.objects.get_subclass(
pk=self.current_stage.configuration_stages.first().pk
)
if stage.configuration_stages.count() == 1:
next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk)
self.logger.debug("Single stage configured, auto-selecting", stage=next_stage)
self.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = next_stage
# Because that normal execution only happens on post, we directly inject it here and
# return it
self.executor.plan.insert_stage(next_stage)
return self.executor.stage_ok()
stages = Stage.objects.filter(
pk__in=self.current_stage.configuration_stages.all()
).select_subclasses()
stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses()
self.executor.plan.context[PLAN_CONTEXT_STAGES] = stages
return super().get(self.request, *args, **kwargs)
@ -309,7 +309,7 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
res = super().post(request, *args, **kwargs)
if (
PLAN_CONTEXT_SELECTED_STAGE in self.executor.plan.context
and self.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
):
self.logger.debug("Got selected stage in context, running that")
stage_pk = self.executor.plan.context.get(PLAN_CONTEXT_SELECTED_STAGE)
@ -351,7 +351,7 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
def cookie_jwt_key(self) -> str:
"""Signing key for MFA Cookie for this stage"""
return sha256(
f"{get_unique_identifier()}:{self.current_stage.pk.hex}".encode("ascii")
f"{get_unique_identifier()}:{self.executor.current_stage.pk.hex}".encode("ascii")
).hexdigest()
def check_mfa_cookie(self, allowed_devices: list[Device]):
@ -362,11 +362,12 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
correct user and with an allowed class"""
if COOKIE_NAME_MFA not in self.request.COOKIES:
return
threshold = timedelta_from_string(self.current_stage.last_auth_threshold)
stage: AuthenticatorValidateStage = self.executor.current_stage
threshold = timedelta_from_string(stage.last_auth_threshold)
latest_allowed = datetime.now() + threshold
try:
payload = decode(self.request.COOKIES[COOKIE_NAME_MFA], self.cookie_jwt_key, ["HS256"])
if payload["stage"] != self.current_stage.pk.hex:
if payload["stage"] != stage.pk.hex:
self.logger.warning("Invalid stage PK")
return
if datetime.fromtimestamp(payload["exp"]) > latest_allowed:
@ -384,14 +385,15 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
"""Set an MFA cookie to allow users to skip MFA validation in this context (browser)
The cookie is JWT which is signed with a hash of the secret key and the UID of the stage"""
delta = timedelta_from_string(self.current_stage.last_auth_threshold)
stage: AuthenticatorValidateStage = self.executor.current_stage
delta = timedelta_from_string(stage.last_auth_threshold)
if delta.total_seconds() < 1:
self.logger.info("Not setting MFA cookie since threshold is not set.")
return self.executor.stage_ok()
expiry = datetime.now() + delta
cookie_payload = {
"device": device.pk,
"stage": self.current_stage.pk.hex,
"stage": stage.pk.hex,
"exp": expiry.timestamp(),
}
response = self.executor.stage_ok()

View File

@ -108,7 +108,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
return registration
class AuthenticatorWebAuthnStageView(ChallengeStageView[AuthenticatorWebAuthnStage]):
class AuthenticatorWebAuthnStageView(ChallengeStageView):
"""WebAuthn stage"""
response_class = AuthenticatorWebAuthnChallengeResponse
@ -116,11 +116,12 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView[AuthenticatorWebAuthnSta
def get_challenge(self, *args, **kwargs) -> Challenge:
# clear session variables prior to starting a new registration
self.request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None)
stage: AuthenticatorWebAuthnStage = self.executor.current_stage
user = self.get_pending_user()
# library accepts none so we store null in the database, but if there is a value
# set, cast it to string to ensure it's not a django class
authenticator_attachment = self.current_stage.authenticator_attachment
authenticator_attachment = stage.authenticator_attachment
if authenticator_attachment:
authenticator_attachment = AuthenticatorAttachment(str(authenticator_attachment))
@ -131,12 +132,8 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView[AuthenticatorWebAuthnSta
user_name=user.username,
user_display_name=user.name,
authenticator_selection=AuthenticatorSelectionCriteria(
resident_key=ResidentKeyRequirement(
str(self.current_stage.resident_key_requirement)
),
user_verification=UserVerificationRequirement(
str(self.current_stage.user_verification)
),
resident_key=ResidentKeyRequirement(str(stage.resident_key_requirement)),
user_verification=UserVerificationRequirement(str(stage.user_verification)),
authenticator_attachment=authenticator_attachment,
),
attestation=AttestationConveyancePreference.DIRECT,

View File

@ -70,7 +70,7 @@ class CaptchaChallengeResponse(ChallengeResponse):
return data
class CaptchaStageView(ChallengeStageView[CaptchaChallenge]):
class CaptchaStageView(ChallengeStageView):
"""Simple captcha checker, logic is handled in django-captcha module"""
response_class = CaptchaChallengeResponse
@ -78,8 +78,8 @@ class CaptchaStageView(ChallengeStageView[CaptchaChallenge]):
def get_challenge(self, *args, **kwargs) -> Challenge:
return CaptchaChallenge(
data={
"js_url": self.current_stage.js_url,
"site_key": self.current_stage.public_key,
"js_url": self.executor.current_stage.js_url,
"site_key": self.executor.current_stage.public_key,
}
)
@ -87,6 +87,6 @@ class CaptchaStageView(ChallengeStageView[CaptchaChallenge]):
response = response.validated_data["token"]
self.executor.plan.context[PLAN_CONTEXT_CAPTCHA] = {
"response": response,
"stage": self.current_stage,
"stage": self.executor.current_stage,
}
return self.executor.stage_ok()

View File

@ -48,7 +48,7 @@ class ConsentChallengeResponse(ChallengeResponse):
token = CharField(required=True)
class ConsentStageView(ChallengeStageView[ConsentStage]):
class ConsentStageView(ChallengeStageView):
"""Simple consent checker."""
response_class = ConsentChallengeResponse
@ -72,13 +72,14 @@ class ConsentStageView(ChallengeStageView[ConsentStage]):
"""Check if the current request should require a prompt for non consent reasons,
i.e. this stage injected from another stage, mode is always requireed or no application
is set."""
current_stage: ConsentStage = self.executor.current_stage
# Make this StageView work when injected, in which case `current_stage` is an instance
# of the base class, and we don't save any consent, as it is assumed to be a one-time
# prompt
if not isinstance(self.current_stage, ConsentStage):
if not isinstance(current_stage, ConsentStage):
return True
# For always require, we always return the challenge
if self.current_stage.mode == ConsentMode.ALWAYS_REQUIRE:
if current_stage.mode == ConsentMode.ALWAYS_REQUIRE:
return True
# at this point we need to check consent from database
if PLAN_CONTEXT_APPLICATION not in self.executor.plan.context:
@ -124,6 +125,7 @@ class ConsentStageView(ChallengeStageView[ConsentStage]):
return self.get(self.request)
if self.should_always_prompt():
return self.executor.stage_ok()
current_stage: ConsentStage = self.executor.current_stage
application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
permissions = self.executor.plan.context.get(
PLAN_CONTEXT_CONSENT_PERMISSIONS, []
@ -137,9 +139,9 @@ class ConsentStageView(ChallengeStageView[ConsentStage]):
)
consent: UserConsent = self.executor.plan.context[PLAN_CONTEXT_CONSENT]
consent.permissions = permissions_string
if self.current_stage.mode == ConsentMode.PERMANENT:
if current_stage.mode == ConsentMode.PERMANENT:
consent.expiring = False
if self.current_stage.mode == ConsentMode.EXPIRING:
consent.expires = now() + timedelta_from_string(self.current_stage.consent_expire_in)
if current_stage.mode == ConsentMode.EXPIRING:
consent.expires = now() + timedelta_from_string(current_stage.consent_expire_in)
consent.save()
return self.executor.stage_ok()

View File

@ -6,10 +6,11 @@ from authentik.flows.stage import StageView
from authentik.stages.deny.models import DenyStage
class DenyStageView(StageView[DenyStage]):
class DenyStageView(StageView):
"""Cancels the current flow"""
def dispatch(self, request: HttpRequest) -> HttpResponse:
"""Cancels the current flow"""
message = self.executor.plan.context.get("deny_message", self.current_stage.deny_message)
stage: DenyStage = self.executor.current_stage
message = self.executor.plan.context.get("deny_message", stage.deny_message)
return self.executor.stage_invalid(message)

View File

@ -30,11 +30,11 @@ class DummyStageView(ChallengeStageView):
return self.executor.stage_ok()
def get_challenge(self, *args, **kwargs) -> Challenge:
if self.current_stage.throw_error:
if self.executor.current_stage.throw_error:
raise SentryIgnoredException("Test error")
return DummyChallenge(
data={
"title": self.current_stage.name,
"name": self.current_stage.name,
"title": self.executor.current_stage.name,
"name": self.executor.current_stage.name,
}
)

View File

@ -46,7 +46,7 @@ class EmailChallengeResponse(ChallengeResponse):
raise ValidationError(detail="email-sent", code="email-sent")
class EmailStageView(ChallengeStageView[EmailStage]):
class EmailStageView(ChallengeStageView):
"""Email stage which sends Email for verification"""
response_class = EmailChallengeResponse
@ -72,10 +72,11 @@ class EmailStageView(ChallengeStageView[EmailStage]):
def get_token(self) -> FlowToken:
"""Get token"""
pending_user = self.get_pending_user()
current_stage: EmailStage = self.executor.current_stage
valid_delta = timedelta(
minutes=self.current_stage.token_expiry + 1
minutes=current_stage.token_expiry + 1
) # + 1 because django timesince always rounds down
identifier = slugify(f"ak-email-stage-{self.current_stage.name}-{str(uuid4())}")
identifier = slugify(f"ak-email-stage-{current_stage.name}-{str(uuid4())}")
# Don't check for validity here, we only care if the token exists
tokens = FlowToken.objects.filter(identifier=identifier)
if not tokens.exists():
@ -104,14 +105,15 @@ class EmailStageView(ChallengeStageView[EmailStage]):
email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
if not email:
email = pending_user.email
current_stage: EmailStage = self.executor.current_stage
token = self.get_token()
# Send mail to user
try:
message = TemplateEmailMessage(
subject=_(self.current_stage.subject),
subject=_(current_stage.subject),
to=[(pending_user.name, email)],
language=pending_user.locale(self.request),
template_name=self.current_stage.template,
template_name=current_stage.template,
template_context={
"url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
"user": pending_user,
@ -119,28 +121,26 @@ class EmailStageView(ChallengeStageView[EmailStage]):
"token": token.key,
},
)
send_mails(self.current_stage, message)
send_mails(current_stage, message)
except TemplateSyntaxError as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message=_("Exception occurred while rendering E-mail template"),
error=exception_to_string(exc),
template=self.current_stage.template,
template=current_stage.template,
).from_http(self.request)
raise StageInvalidException from exc
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Check if the user came back from the email link to verify
restore_token: FlowToken | None = self.executor.plan.context.get(
PLAN_CONTEXT_IS_RESTORED, None
)
restore_token: FlowToken = self.executor.plan.context.get(PLAN_CONTEXT_IS_RESTORED, None)
user = self.get_pending_user()
if restore_token:
if restore_token.user != user:
self.logger.warning("Flow token for non-matching user, denying request")
return self.executor.stage_invalid()
messages.success(request, _("Successfully verified Email."))
if self.current_stage.activate_user_on_success:
if self.executor.current_stage.activate_user_on_success:
user.is_active = True
user.save()
return self.executor.stage_ok()

View File

@ -27,7 +27,6 @@ class IdentificationStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + [
"user_fields",
"password_stage",
"captcha_stage",
"case_insensitive_matching",
"show_matched_user",
"enrollment_flow",

View File

@ -1,26 +0,0 @@
# Generated by Django 5.0.8 on 2024-08-24 12:58
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_captcha", "0003_captchastage_error_on_invalid_score_and_more"),
("authentik_stages_identification", "0014_identificationstage_pretend"),
]
operations = [
migrations.AddField(
model_name="identificationstage",
name="captcha_stage",
field=models.ForeignKey(
default=None,
help_text="When set, the captcha element is shown on the identification stage.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_stages_captcha.captchastage",
),
),
]

View File

@ -8,7 +8,6 @@ from rest_framework.serializers import BaseSerializer
from authentik.core.models import Source
from authentik.flows.models import Flow, Stage
from authentik.stages.captcha.models import CaptchaStage
from authentik.stages.password.models import PasswordStage
@ -43,15 +42,6 @@ class IdentificationStage(Stage):
),
),
)
captcha_stage = models.ForeignKey(
CaptchaStage,
null=True,
default=None,
on_delete=models.SET_NULL,
help_text=_(
("When set, the captcha element is shown on the identification stage."),
),
)
case_insensitive_matching = models.BooleanField(
default=True,

View File

@ -30,14 +30,9 @@ from authentik.lib.utils.urls import reverse_with_qs
from authentik.root.middleware import ClientIPMiddleware
from authentik.sources.oauth.types.apple import AppleLoginChallenge
from authentik.sources.plex.models import PlexAuthenticationChallenge
from authentik.stages.captcha.stage import (
CaptchaChallenge,
CaptchaChallengeResponse,
CaptchaStageView,
)
from authentik.stages.identification.models import IdentificationStage
from authentik.stages.identification.signals import identification_failed
from authentik.stages.password.stage import PasswordChallenge, PasswordStageView, authenticate
from authentik.stages.password.stage import authenticate
@extend_schema_field(
@ -68,8 +63,8 @@ class IdentificationChallenge(Challenge):
"""Identification challenges with all UI elements"""
user_fields = ListField(child=CharField(), allow_empty=True, allow_null=True)
password_stage = PasswordChallenge(required=False)
captcha_stage = CaptchaChallenge(required=False)
password_fields = BooleanField()
allow_show_password = BooleanField(default=False)
application_pre = CharField(required=False)
flow_designation = ChoiceField(FlowDesignation.choices)
@ -89,7 +84,6 @@ class IdentificationChallengeResponse(ChallengeResponse):
uid_field = CharField()
password = CharField(required=False, allow_blank=True, allow_null=True)
component = CharField(default="ak-stage-identification")
captcha = CaptchaChallengeResponse(required=False)
pre_user: User | None = None
@ -134,50 +128,49 @@ class IdentificationChallengeResponse(ChallengeResponse):
return attrs
raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user
if current_stage.password_stage:
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
description="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
print(attrs)
# if current_stage.captcha_stage:
# captcha = CaptchaStageView(self.stage.executor)
# captcha.stage = current_stage.captcha_stage
# captcha.challenge_valid(attrs.get("captcha"))
if not current_stage.password_stage:
# No password stage select, don't validate the password
return attrs
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
description="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
return attrs
class IdentificationStageView(ChallengeStageView[IdentificationStage]):
class IdentificationStageView(ChallengeStageView):
"""Form to identify the user"""
response_class = IdentificationChallengeResponse
def get_user(self, uid_value: str) -> User | None:
"""Find user instance. Returns None if no user was found."""
current_stage: IdentificationStage = self.executor.current_stage
query = Q()
for search_field in self.current_stage.user_fields:
for search_field in current_stage.user_fields:
model_field = {
"email": "email",
"username": "username",
"upn": "attributes__upn",
}[search_field]
if self.current_stage.case_insensitive_matching:
if current_stage.case_insensitive_matching:
model_field += "__iexact"
else:
model_field += "__exact"
@ -198,12 +191,16 @@ class IdentificationStageView(ChallengeStageView[IdentificationStage]):
return _("Continue")
def get_challenge(self) -> Challenge:
current_stage: IdentificationStage = self.executor.current_stage
challenge = IdentificationChallenge(
data={
"component": "ak-stage-identification",
"primary_action": self.get_primary_action(),
"user_fields": self.current_stage.user_fields,
"show_source_labels": self.current_stage.show_source_labels,
"user_fields": current_stage.user_fields,
"password_fields": bool(current_stage.password_stage),
"allow_show_password": bool(current_stage.password_stage)
and current_stage.password_stage.allow_show_password,
"show_source_labels": current_stage.show_source_labels,
"flow_designation": self.executor.flow.designation,
}
)
@ -215,39 +212,29 @@ class IdentificationStageView(ChallengeStageView[IdentificationStage]):
).name
get_qs = self.request.session.get(SESSION_KEY_GET, self.request.GET)
# Check for related enrollment and recovery flow, add URL to view
if self.current_stage.enrollment_flow:
if current_stage.enrollment_flow:
challenge.initial_data["enroll_url"] = reverse_with_qs(
"authentik_core:if-flow",
query=get_qs,
kwargs={"flow_slug": self.current_stage.enrollment_flow.slug},
kwargs={"flow_slug": current_stage.enrollment_flow.slug},
)
if self.current_stage.recovery_flow:
if current_stage.recovery_flow:
challenge.initial_data["recovery_url"] = reverse_with_qs(
"authentik_core:if-flow",
query=get_qs,
kwargs={"flow_slug": self.current_stage.recovery_flow.slug},
kwargs={"flow_slug": current_stage.recovery_flow.slug},
)
if self.current_stage.passwordless_flow:
if current_stage.passwordless_flow:
challenge.initial_data["passwordless_url"] = reverse_with_qs(
"authentik_core:if-flow",
query=get_qs,
kwargs={"flow_slug": self.current_stage.passwordless_flow.slug},
kwargs={"flow_slug": current_stage.passwordless_flow.slug},
)
if self.current_stage.password_stage:
password = PasswordStageView(self.executor, self.current_stage.captcha_stage)
password_challenge = password.get_challenge()
password_challenge.is_valid()
challenge.initial_data["password_stage"] = password_challenge.data
if self.current_stage.captcha_stage:
captcha = CaptchaStageView(self.executor, self.current_stage.captcha_stage)
captcha_challenge = captcha.get_challenge()
captcha_challenge.is_valid()
challenge.initial_data["captcha_stage"] = captcha_challenge.data
# Check all enabled source, add them if they have a UI Login button.
ui_sources = []
sources: list[Source] = (
self.current_stage.sources.filter(enabled=True).order_by("name").select_subclasses()
current_stage.sources.filter(enabled=True).order_by("name").select_subclasses()
)
for source in sources:
ui_login_button = source.ui_login_button(self.request)
@ -262,7 +249,8 @@ class IdentificationStageView(ChallengeStageView[IdentificationStage]):
def challenge_valid(self, response: IdentificationChallengeResponse) -> HttpResponse:
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = response.pre_user
if not self.current_stage.show_matched_user:
current_stage: IdentificationStage = self.executor.current_stage
if not current_stage.show_matched_user:
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = (
response.validated_data.get("uid_field")
)

View File

@ -17,7 +17,7 @@ INVITATION_IN_EFFECT = "invitation_in_effect"
INVITATION = "invitation"
class InvitationStageView(StageView[InvitationStage]):
class InvitationStageView(StageView):
"""Finalise Authentication flow by logging the user in"""
def get_token(self) -> str | None:
@ -52,10 +52,11 @@ class InvitationStageView(StageView[InvitationStage]):
def dispatch(self, request: HttpRequest) -> HttpResponse:
"""Apply data to the current flow based on a URL"""
stage: InvitationStage = self.executor.current_stage
invite = self.get_invite()
if not invite:
if self.current_stage.continue_flow_without_invitation:
if stage.continue_flow_without_invitation:
return self.executor.stage_ok()
return self.executor.stage_invalid(_("Invalid invite/invite not found"))

View File

@ -130,7 +130,7 @@ class PasswordChallengeResponse(ChallengeResponse):
return password
class PasswordStageView(ChallengeStageView[PasswordStage]):
class PasswordStageView(ChallengeStageView):
"""Authentication stage which authenticates against django's AuthBackend"""
response_class = PasswordChallengeResponse
@ -138,7 +138,7 @@ class PasswordStageView(ChallengeStageView[PasswordStage]):
def get_challenge(self) -> Challenge:
challenge = PasswordChallenge(
data={
"allow_show_password": self.current_stage.allow_show_password,
"allow_show_password": self.executor.current_stage.allow_show_password,
}
)
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
@ -154,9 +154,10 @@ class PasswordStageView(ChallengeStageView[PasswordStage]):
if SESSION_KEY_INVALID_TRIES not in self.request.session:
self.request.session[SESSION_KEY_INVALID_TRIES] = 0
self.request.session[SESSION_KEY_INVALID_TRIES] += 1
current_stage: PasswordStage = self.executor.current_stage
if (
self.request.session[SESSION_KEY_INVALID_TRIES]
>= self.current_stage.failed_attempts_before_cancel
>= current_stage.failed_attempts_before_cancel
):
self.logger.debug("User has exceeded maximum tries")
del self.request.session[SESSION_KEY_INVALID_TRIES]

View File

@ -222,7 +222,7 @@ class PromptStageView(ChallengeStageView):
return serializers
def get_challenge(self, *args, **kwargs) -> Challenge:
fields: list[Prompt] = list(self.current_stage.fields.all().order_by("order"))
fields: list[Prompt] = list(self.executor.current_stage.fields.all().order_by("order"))
context_prompt = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {})
serializers = self.get_prompt_challenge_fields(fields, context_prompt)
challenge = PromptChallenge(
@ -239,7 +239,7 @@ class PromptStageView(ChallengeStageView):
instance=None,
data=data,
request=self.request,
stage_instance=self.current_stage,
stage_instance=self.executor.current_stage,
stage=self,
plan=self.executor.plan,
user=self.get_pending_user(),

View File

@ -7,10 +7,9 @@ from django.utils.translation import gettext as _
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView
from authentik.stages.user_delete.models import UserDeleteStage
class UserDeleteStageView(StageView[UserDeleteStage]):
class UserDeleteStageView(StageView):
"""Finalise unenrollment flow by deleting the user object."""
def dispatch(self, request: HttpRequest) -> HttpResponse:

View File

@ -39,7 +39,7 @@ class UserLoginChallengeResponse(ChallengeResponse):
remember_me = BooleanField(required=True)
class UserLoginStageView(ChallengeStageView[UserLoginStage]):
class UserLoginStageView(ChallengeStageView):
"""Finalise Authentication flow by logging the user in"""
response_class = UserLoginChallengeResponse
@ -49,7 +49,8 @@ class UserLoginStageView(ChallengeStageView[UserLoginStage]):
def dispatch(self, request: HttpRequest) -> HttpResponse:
"""Check for remember_me, and do login"""
if timedelta_from_string(self.current_stage.remember_me_offset).total_seconds() > 0:
stage: UserLoginStage = self.executor.current_stage
if timedelta_from_string(stage.remember_me_offset).total_seconds() > 0:
return super().dispatch(request)
return self.do_login(request)
@ -58,9 +59,9 @@ class UserLoginStageView(ChallengeStageView[UserLoginStage]):
def set_session_duration(self, remember: bool) -> timedelta:
"""Update the sessions' expiry"""
delta = timedelta_from_string(self.current_stage.session_duration)
delta = timedelta_from_string(self.executor.current_stage.session_duration)
if remember:
offset = timedelta_from_string(self.current_stage.remember_me_offset)
offset = timedelta_from_string(self.executor.current_stage.remember_me_offset)
delta = delta + offset
if delta.total_seconds() == 0:
self.request.session.set_expiry(0)
@ -70,9 +71,11 @@ class UserLoginStageView(ChallengeStageView[UserLoginStage]):
def set_session_ip(self):
"""Set the sessions' last IP and session bindings"""
stage: UserLoginStage = self.executor.current_stage
self.request.session[SESSION_KEY_LAST_IP] = ClientIPMiddleware.get_client_ip(self.request)
self.request.session[SESSION_KEY_BINDING_NET] = self.current_stage.network_binding
self.request.session[SESSION_KEY_BINDING_GEO] = self.current_stage.geoip_binding
self.request.session[SESSION_KEY_BINDING_NET] = stage.network_binding
self.request.session[SESSION_KEY_BINDING_GEO] = stage.geoip_binding
def do_login(self, request: HttpRequest, remember: bool = False) -> HttpResponse:
"""Attach the currently pending user to the current session"""
@ -108,7 +111,7 @@ class UserLoginStageView(ChallengeStageView[UserLoginStage]):
# as sources show their own success messages
if not self.executor.plan.context.get(PLAN_CONTEXT_SOURCE, None):
messages.success(self.request, _("Successfully logged in!"))
if self.current_stage.terminate_other_sessions:
if self.executor.current_stage.terminate_other_sessions:
AuthenticatedSession.objects.filter(
user=user,
).exclude(session_key=self.request.session.session_key).delete()

View File

@ -4,10 +4,9 @@ from django.contrib.auth import logout
from django.http import HttpRequest, HttpResponse
from authentik.flows.stage import StageView
from authentik.stages.user_logout.models import UserLogoutStage
class UserLogoutStageView(StageView[UserLogoutStage]):
class UserLogoutStageView(StageView):
"""Finalise Authentication flow by logging the user in"""
def dispatch(self, request: HttpRequest) -> HttpResponse:

View File

@ -55,7 +55,7 @@ class UserWriteStageView(StageView):
"""Ensure a user exists"""
user_created = False
path = self.executor.plan.context.get(
PLAN_CONTEXT_USER_PATH, self.current_stage.user_path_template
PLAN_CONTEXT_USER_PATH, self.executor.current_stage.user_path_template
)
if path == "":
path = User.default_path()
@ -64,11 +64,11 @@ class UserWriteStageView(StageView):
user_type = UserTypes(
self.executor.plan.context.get(
PLAN_CONTEXT_USER_TYPE,
self.current_stage.user_type,
self.executor.current_stage.user_type,
)
)
except ValueError:
user_type = self.current_stage.user_type
user_type = self.executor.current_stage.user_type
if user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
user_type = UserTypes.SERVICE_ACCOUNT
@ -76,12 +76,12 @@ class UserWriteStageView(StageView):
self.executor.plan.context.setdefault(PLAN_CONTEXT_PENDING_USER, self.request.user)
if (
PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context
or self.current_stage.user_creation_mode == UserCreationMode.ALWAYS_CREATE
or self.executor.current_stage.user_creation_mode == UserCreationMode.ALWAYS_CREATE
):
if self.current_stage.user_creation_mode == UserCreationMode.NEVER_CREATE:
if self.executor.current_stage.user_creation_mode == UserCreationMode.NEVER_CREATE:
return None, False
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
is_active=not self.current_stage.create_users_as_inactive,
is_active=not self.executor.current_stage.create_users_as_inactive,
path=path,
type=user_type,
)
@ -180,8 +180,8 @@ class UserWriteStageView(StageView):
try:
with transaction.atomic():
user.save()
if self.current_stage.create_users_group:
user.ak_groups.add(self.current_stage.create_users_group)
if self.executor.current_stage.create_users_group:
user.ak_groups.add(self.executor.current_stage.create_users_group)
if PLAN_CONTEXT_GROUPS in self.executor.plan.context:
user.ak_groups.add(*self.executor.plan.context[PLAN_CONTEXT_GROUPS])
except (IntegrityError, ValueError, TypeError, InternalError) as exc:

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2024.6.4 Blueprint schema",
"title": "authentik 2024.8.2 Blueprint schema",
"required": [
"version",
"entries"
@ -10091,11 +10091,6 @@
"title": "Password stage",
"description": "When set, shows a password field, instead of showing the password field as separate step."
},
"captcha_stage": {
"type": "integer",
"title": "Captcha stage",
"description": "When set, the captcha element is shown on the identification stage."
},
"case_insensitive_matching": {
"type": "boolean",
"title": "Case insensitive matching",

View File

@ -31,7 +31,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.4}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.2}
restart: unless-stopped
command: server
environment:
@ -52,7 +52,7 @@ services:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.4}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.2}
restart: unless-stopped
command: worker
environment:

6
go.mod
View File

@ -18,18 +18,18 @@ require (
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0
github.com/gorilla/websocket v1.5.3
github.com/jellydator/ttlcache/v3 v3.2.0
github.com/jellydator/ttlcache/v3 v3.2.1
github.com/mitchellh/mapstructure v1.5.0
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.20.1
github.com/prometheus/client_golang v1.20.2
github.com/redis/go-redis/v9 v9.6.1
github.com/sethvargo/go-envconfig v1.1.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024063.13
goauthentik.io/api/v3 v3.2024064.1
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.22.0
golang.org/x/sync v0.8.0

16
go.sum
View File

@ -200,8 +200,8 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE=
github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
github.com/jellydator/ttlcache/v3 v3.2.1 h1:eS8ljnYY7BllYGkXw/TfczWZrXUu/CH7SIkC6ugn9Js=
github.com/jellydator/ttlcache/v3 v3.2.1/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@ -239,8 +239,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.1 h1:IMJXHOD6eARkQpxo8KkhgEVFlBNm+nkrFUyGlIu7Na8=
github.com/prometheus/client_golang v1.20.1/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
@ -297,10 +297,10 @@ go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucg
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2024063.13 h1:zWFlrr+8NOaQOCPSRV1FhbDJ58+BPa9BqjNvl4T//s8=
goauthentik.io/api/v3 v3.2024063.13/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2024064.1 h1:vxquklgDGD+nGFhWRAsQ7ezQKg17MRq6bzEk25fbsb4=
goauthentik.io/api/v3 v3.2024064.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

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

View File

@ -35,10 +35,11 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
req PaginatorRequest[Treq, Tres],
opts PaginatorOptions,
) ([]Tobj, error) {
var bfreq, cfreq interface{}
fetchOffset := func(page int32) (Tres, error) {
req.Page(page)
req.PageSize(int32(opts.PageSize))
res, _, err := req.Execute()
bfreq = req.Page(page)
cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize))
res, _, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
if err != nil {
opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page")
}

View File

@ -0,0 +1,26 @@
package ak
// func Test_PaginatorCompile(t *testing.T) {
// req := api.ApiCoreUsersListRequest{}
// Paginator(req, PaginatorOptions{
// PageSize: 100,
// })
// }
// func Test_PaginatorCompileExplicit(t *testing.T) {
// req := api.ApiCoreUsersListRequest{}
// Paginator[
// api.User,
// api.ApiCoreUsersListRequest,
// *api.PaginatedUserList,
// ](req, PaginatorOptions{
// PageSize: 100,
// })
// }
// func Test_PaginatorCompileOther(t *testing.T) {
// req := api.ApiOutpostsProxyListRequest{}
// Paginator(req, PaginatorOptions{
// PageSize: 100,
// })
// }

View File

@ -96,7 +96,7 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
return ldap.LDAPResultOperationsError, nil
}
flags.UserPk = userInfo.User.Pk
flags.CanSearch = access.HasSearchPermission != nil
flags.CanSearch = access.GetHasSearchPermission()
db.si.SetFlags(req.BindDN, &flags)
if flags.CanSearch {
req.Log().Debug("Allowed access to search")

View File

@ -193,7 +193,17 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server) (*A
})
mux.HandleFunc("/outpost.goauthentik.io/start", func(w http.ResponseWriter, r *http.Request) {
a.handleAuthStart(w, r, "")
fwd := ""
// This should only really be hit for nginx forward_auth
// as for that the auth start redirect URL is generated by the
// reverse proxy, and as such we won't have a request we just
// denied to reference for final URL
rd, ok := a.checkRedirectParam(r)
if ok {
a.log.WithField("rd", rd).Trace("Setting redirect")
fwd = rd
}
a.handleAuthStart(w, r, fwd)
})
mux.HandleFunc("/outpost.goauthentik.io/callback", a.handleAuthCallback)
mux.HandleFunc("/outpost.goauthentik.io/sign_out", a.handleSignOut)

View File

@ -15,36 +15,6 @@ const (
LogoutSignature = "X-authentik-logout"
)
func (a *Application) checkRedirectParam(r *http.Request) (string, bool) {
rd := r.URL.Query().Get(redirectParam)
if rd == "" {
return "", false
}
u, err := url.Parse(rd)
if err != nil {
a.log.WithError(err).Warning("Failed to parse redirect URL")
return "", false
}
// Check to make sure we only redirect to allowed places
if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE {
ext, err := url.Parse(a.proxyConfig.ExternalHost)
if err != nil {
return "", false
}
ext.Scheme = ""
if !strings.Contains(u.String(), ext.String()) {
a.log.WithField("url", u.String()).WithField("ext", ext.String()).Warning("redirect URI did not contain external host")
return "", false
}
} else {
if !strings.HasSuffix(u.Host, *a.proxyConfig.CookieDomain) {
a.log.WithField("host", u.Host).WithField("dom", *a.proxyConfig.CookieDomain).Warning("redirect URI Host was not included in cookie domain")
return "", false
}
}
return u.String(), true
}
func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request, fwd string) {
state, err := a.createState(r, fwd)
if err != nil {

View File

@ -5,10 +5,13 @@ import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/securecookie"
"github.com/mitchellh/mapstructure"
"goauthentik.io/api/v3"
)
type OAuthState struct {
@ -27,6 +30,44 @@ func (oas *OAuthState) GetAudience() (jwt.ClaimStrings, error) { return ni
var base32RawStdEncoding = base32.StdEncoding.WithPadding(base32.NoPadding)
// Validate that the given redirect parameter (?rd=...) is valid and can be used
// For proxy/forward_single this checks that if the `rd` param has a Hostname (and is a full URL)
// the hostname matches what's configured, or no hostname must be given
// For forward_domain this checks if the domain of the URL in `rd` ends with the configured domain
func (a *Application) checkRedirectParam(r *http.Request) (string, bool) {
rd := r.URL.Query().Get(redirectParam)
if rd == "" {
return "", false
}
u, err := url.Parse(rd)
if err != nil {
a.log.WithError(err).Warning("Failed to parse redirect URL")
return "", false
}
// Check to make sure we only redirect to allowed places
if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE {
ext, err := url.Parse(a.proxyConfig.ExternalHost)
if err != nil {
return "", false
}
// Either hostname needs to match the configured domain, or host name must be empty for just a path
if u.Host == "" {
u.Host = ext.Host
u.Scheme = ext.Scheme
}
if u.Host != ext.Host {
a.log.WithField("url", u.String()).WithField("ext", ext.String()).Warning("redirect URI did not contain external host")
return "", false
}
} else {
if !strings.HasSuffix(u.Host, *a.proxyConfig.CookieDomain) {
a.log.WithField("host", u.Host).WithField("dom", *a.proxyConfig.CookieDomain).Warning("redirect URI Host was not included in cookie domain")
return "", false
}
}
return u.String(), true
}
func (a *Application) createState(r *http.Request, fwd string) (string, error) {
s, _ := a.sessions.Get(r, a.SessionName())
if s.ID == "" {
@ -39,17 +80,6 @@ func (a *Application) createState(r *http.Request, fwd string) (string, error) {
SessionID: s.ID,
Redirect: fwd,
}
if fwd == "" {
// This should only really be hit for nginx forward_auth
// as for that the auth start redirect URL is generated by the
// reverse proxy, and as such we won't have a request we just
// denied to reference for final URL
rd, ok := a.checkRedirectParam(r)
if ok {
a.log.WithField("rd", rd).Trace("Setting redirect")
st.Redirect = rd
}
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, st)
tokenString, err := token.SignedString([]byte(a.proxyConfig.GetCookieSecret()))
if err != nil {

View File

@ -8,25 +8,45 @@ import (
"goauthentik.io/api/v3"
)
func TestCheckRedirectParam(t *testing.T) {
func TestCheckRedirectParam_None(t *testing.T) {
a := newTestApplication()
// Test no rd param
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start", nil)
rd, ok := a.checkRedirectParam(req)
assert.Equal(t, false, ok)
assert.Equal(t, "", rd)
}
req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://google.com", nil)
func TestCheckRedirectParam_Invalid(t *testing.T) {
a := newTestApplication()
// Test invalid rd param
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://google.com", nil)
rd, ok = a.checkRedirectParam(req)
rd, ok := a.checkRedirectParam(req)
assert.Equal(t, false, ok)
assert.Equal(t, "", rd)
}
req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://ext.t.goauthentik.io/test?foo", nil)
func TestCheckRedirectParam_ValidFull(t *testing.T) {
a := newTestApplication()
// Test valid full rd param
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://ext.t.goauthentik.io/test?foo", nil)
rd, ok = a.checkRedirectParam(req)
rd, ok := a.checkRedirectParam(req)
assert.Equal(t, true, ok)
assert.Equal(t, "https://ext.t.goauthentik.io/test?foo", rd)
}
func TestCheckRedirectParam_ValidPartial(t *testing.T) {
a := newTestApplication()
// Test valid partial rd param
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=/test?foo", nil)
rd, ok := a.checkRedirectParam(req)
assert.Equal(t, true, ok)
assert.Equal(t, "https://ext.t.goauthentik.io/test?foo", rd)

View File

@ -1,5 +1,5 @@
{
"name": "@goauthentik/authentik",
"version": "2024.6.4",
"version": "2024.8.2",
"private": true
}

66
poetry.lock generated
View File

@ -1053,38 +1053,38 @@ toml = ["tomli"]
[[package]]
name = "cryptography"
version = "43.0.0"
version = "43.0.1"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
{file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"},
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"},
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"},
{file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"},
{file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"},
{file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"},
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"},
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"},
{file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"},
{file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"},
{file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"},
{file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"},
{file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"},
{file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"},
{file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"},
{file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"},
{file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"},
{file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"},
{file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"},
{file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"},
{file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"},
{file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"},
{file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"},
{file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"},
{file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"},
{file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"},
{file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"},
{file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"},
{file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"},
{file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"},
{file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"},
{file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"},
{file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"},
{file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"},
{file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"},
{file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"},
{file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"},
{file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"},
]
[package.dependencies]
@ -1097,7 +1097,7 @@ nox = ["nox"]
pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
[[package]]
@ -1312,17 +1312,17 @@ django = ">=3"
[[package]]
name = "django-pglock"
version = "1.5.1"
version = "1.6.0"
description = "Postgres locking routines and lock table access."
optional = false
python-versions = "<4,>=3.8.0"
files = [
{file = "django_pglock-1.5.1-py3-none-any.whl", hash = "sha256:d3b977922abbaffd43968714b69cdab7453866adf2b0695fb497491748d7bc67"},
{file = "django_pglock-1.5.1.tar.gz", hash = "sha256:291903d5d877b68558003e1d64d764ebd5590344ba3b7aa1d5127df5947869b1"},
{file = "django_pglock-1.6.0-py3-none-any.whl", hash = "sha256:41c98d0bd3738d11e6eaefcc3e5146028f118a593ac58c13d663b751170f01de"},
{file = "django_pglock-1.6.0.tar.gz", hash = "sha256:724450ecc9886f39af599c477d84ad086545a5373215ef7a670cd25faca25a61"},
]
[package.dependencies]
django = ">=3"
django = ">=4"
django-pgactivity = ">=1.2,<2"
[[package]]

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "authentik"
version = "2024.6.4"
version = "2024.8.2"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2024.6.4
version: 2024.8.2
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -40457,10 +40457,11 @@ components:
items:
type: string
nullable: true
password_stage:
$ref: '#/components/schemas/PasswordChallenge'
captcha_stage:
$ref: '#/components/schemas/CaptchaChallenge'
password_fields:
type: boolean
allow_show_password:
type: boolean
default: false
application_pre:
type: string
flow_designation:
@ -40481,6 +40482,7 @@ components:
type: boolean
required:
- flow_designation
- password_fields
- primary_action
- show_source_labels
- user_fields
@ -40498,8 +40500,6 @@ components:
password:
type: string
nullable: true
captcha:
$ref: '#/components/schemas/CaptchaChallengeResponseRequest'
required:
- uid_field
IdentificationStage:
@ -40545,12 +40545,6 @@ components:
nullable: true
description: When set, shows a password field, instead of showing the password
field as separate step.
captcha_stage:
type: string
format: uuid
nullable: true
description: When set, the captcha element is shown on the identification
stage.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.
@ -40619,12 +40613,6 @@ components:
nullable: true
description: When set, shows a password field, instead of showing the password
field as separate step.
captcha_stage:
type: string
format: uuid
nullable: true
description: When set, the captcha element is shown on the identification
stage.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.
@ -45757,12 +45745,6 @@ components:
nullable: true
description: When set, shows a password field, instead of showing the password
field as separate step.
captcha_stage:
type: string
format: uuid
nullable: true
description: When set, the captcha element is shown on the identification
stage.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.

View File

@ -11,6 +11,7 @@ from ldap3.core.exceptions import LDAPInvalidCredentialsResult
from authentik.blueprints.tests import apply_blueprint, reconcile_app
from authentik.core.models import Application, User
from authentik.core.tests.utils import create_test_user
from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id
@ -331,6 +332,83 @@ class TestProviderLDAP(SeleniumTestCase):
]
self.assert_list_dict_equal(expected, response)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@reconcile_app("authentik_tenants")
@reconcile_app("authentik_outposts")
def test_ldap_bind_search_no_perms(self):
"""Test simple bind + search"""
user = create_test_user()
self._prepare()
server = Server("ldap://localhost:3389", get_info=ALL)
_connection = Connection(
server,
raise_exceptions=True,
user=f"cn={user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
password=user.username,
)
_connection.bind()
self.assertTrue(
Event.objects.filter(
action=EventAction.LOGIN,
user={
"pk": user.pk,
"email": user.email,
"username": user.username,
},
)
)
_connection.search(
"ou=Users,DC=ldaP,dc=goauthentik,dc=io",
"(objectClass=user)",
search_scope=SUBTREE,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
)
response: list = _connection.response
# Remove raw_attributes to make checking easier
for obj in response:
del obj["raw_attributes"]
del obj["raw_dn"]
obj["attributes"] = dict(obj["attributes"])
expected = [
{
"dn": f"cn={user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
"attributes": {
"cn": user.username,
"sAMAccountName": user.username,
"uid": user.uid,
"name": user.name,
"displayName": user.name,
"sn": user.name,
"mail": user.email,
"objectClass": [
"top",
"person",
"organizationalPerson",
"inetOrgPerson",
"user",
"posixAccount",
"goauthentik.io/ldap/user",
],
"uidNumber": 2000 + user.pk,
"gidNumber": 2000 + user.pk,
"memberOf": [
f"cn={group.name},ou=groups,dc=ldap,dc=goauthentik,dc=io"
for group in user.ak_groups.all()
],
"homeDirectory": f"/home/{user.username}",
"ak-active": True,
"ak-superuser": False,
},
"type": "searchResEntry",
},
]
self.assert_list_dict_equal(expected, response)
def assert_list_dict_equal(self, expected: list[dict], actual: list[dict], match_key="dn"):
"""Assert a list of dictionaries is identical, ignoring the ordering of items"""
self.assertEqual(len(expected), len(actual))

9519
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@
"guacamole-common-js": "^1.5.0",
"lit": "^3.2.0",
"md-front-matter": "^1.0.4",
"mermaid": "^10.9.1",
"mermaid": "^11.0.2",
"rapidoc": "^9.3.4",
"showdown": "^2.1.0",
"style-mod": "^4.1.2",
@ -51,7 +51,7 @@
"@babel/preset-typescript": "^7.24.7",
"@changesets/cli": "^2.27.5",
"@custom-elements-manifest/analyzer": "^0.10.2",
"@eslint/js": "^9.9.0",
"@eslint/js": "^9.9.1",
"@genesiscommunitysuccess/custom-elements-lsp": "^5.0.3",
"@hcaptcha/types": "^1.0.4",
"@jeysal/storybook-addon-css-user-preferences": "^0.2.0",
@ -91,7 +91,6 @@
"glob": "^11.0.0",
"globals": "^15.9.0",
"lit-analyzer": "^2.0.3",
"lockfile-lint": "^4.14.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"pseudolocale": "^2.1.0",
@ -101,10 +100,10 @@
"rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.1.11",
"storybook-addon-mock": "^5.0.0",
"syncpack": "^12.3.3",
"syncpack": "^13.0.0",
"ts-lit-plugin": "^2.0.2",
"ts-node": "^10.9.2",
"tslib": "^2.6.3",
"tslib": "^2.7.0",
"turnstile-types": "^1.2.2",
"typescript": "^5.5.4",
"typescript-eslint": "^8.2.0",
@ -257,7 +256,9 @@
]
},
"lint:lockfile": {
"command": "lockfile-lint --path package.json --type npm --allowed-hosts npm --validate-https"
"__comment": "The lockfile-lint package does not have an option to ensure resolved hashes are set everywhere",
"shell": true,
"command": "[ -z \"$(jq -r '.packages | to_entries[] | select((.key | startswith(\"node_modules\")) and (.value | has(\"resolved\") | not)) | .key' < package-lock.json)\" ]"
},
"lint:lockfiles": {
"dependencies": [

View File

@ -14,7 +14,7 @@
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-swc": "^0.3.1",
"@swc/cli": "^0.4.0",
"@swc/core": "^1.7.14",
"@swc/core": "^1.7.18",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/jquery": "^3.5.30",
"lockfile-lint": "^4.14.0",
@ -25,7 +25,7 @@
},
"license": "MIT",
"optionalDependencies": {
"@swc/core": "^1.7.14",
"@swc/core": "^1.7.18",
"@swc/core-darwin-arm64": "^1.6.13",
"@swc/core-darwin-x64": "^1.6.13",
"@swc/core-linux-arm-gnueabihf": "^1.6.13",

View File

@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"@goauthentik/api": "^2024.6.3-1724337552",
"@goauthentik/api": "^2024.6.3-1724414734",
"base64-js": "^1.5.1",
"bootstrap": "^4.6.1",
"formdata-polyfill": "^4.0.10",
@ -21,16 +21,16 @@
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-swc": "^0.3.1",
"@swc/cli": "^0.4.0",
"@swc/core": "^1.7.14",
"@swc/core": "^1.7.18",
"@types/jquery": "^3.5.30",
"rollup": "^4.21.0",
"rollup-plugin-copy": "^3.5.0"
}
},
"node_modules/@goauthentik/api": {
"version": "2024.6.3-1724337552",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.3-1724337552.tgz",
"integrity": "sha512-siu5qJqUt13iUPsLI0RfieVkDU8IMhuP2i5C/RRqY6oek0z+srSom9UTBAh6n6a2pTTNQO3clE2zxvAIJPahVg=="
"version": "2024.6.3-1724414734",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.3-1724414734.tgz",
"integrity": "sha512-2fLKwOh2Znc/unD8Q2U4G0g5QFM4jVqC95e5VRWWVnzp3xB7JWfEDBcRdwyv5PxCdmjBUkvbiul0kiuRwqBf4w=="
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
@ -491,9 +491,9 @@
}
},
"node_modules/@swc/core": {
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.14.tgz",
"integrity": "sha512-9aeXeifnyuvc2pcuuhPQgVUwdpGEzZ+9nJu0W8/hNl/aESFsJGR5i9uQJRGu0atoNr01gK092fvmqMmQAPcKow==",
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.18.tgz",
"integrity": "sha512-qL9v5N5S38ijmqiQRvCFUUx2vmxWT/JJ2rswElnyaHkOHuVoAFhBB90Ywj4RKjh3R0zOjhEcemENTyF3q3G6WQ==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@ -508,16 +508,16 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.7.14",
"@swc/core-darwin-x64": "1.7.14",
"@swc/core-linux-arm-gnueabihf": "1.7.14",
"@swc/core-linux-arm64-gnu": "1.7.14",
"@swc/core-linux-arm64-musl": "1.7.14",
"@swc/core-linux-x64-gnu": "1.7.14",
"@swc/core-linux-x64-musl": "1.7.14",
"@swc/core-win32-arm64-msvc": "1.7.14",
"@swc/core-win32-ia32-msvc": "1.7.14",
"@swc/core-win32-x64-msvc": "1.7.14"
"@swc/core-darwin-arm64": "1.7.18",
"@swc/core-darwin-x64": "1.7.18",
"@swc/core-linux-arm-gnueabihf": "1.7.18",
"@swc/core-linux-arm64-gnu": "1.7.18",
"@swc/core-linux-arm64-musl": "1.7.18",
"@swc/core-linux-x64-gnu": "1.7.18",
"@swc/core-linux-x64-musl": "1.7.18",
"@swc/core-win32-arm64-msvc": "1.7.18",
"@swc/core-win32-ia32-msvc": "1.7.18",
"@swc/core-win32-x64-msvc": "1.7.18"
},
"peerDependencies": {
"@swc/helpers": "*"
@ -529,9 +529,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.14.tgz",
"integrity": "sha512-V0OUXjOH+hdGxDYG8NkQzy25mKOpcNKFpqtZEzLe5V/CpLJPnpg1+pMz70m14s9ZFda9OxsjlvPbg1FLUwhgIQ==",
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.18.tgz",
"integrity": "sha512-MwLc5U+VGPMZm8MjlFBjEB2wyT1EK0NNJ3tn+ps9fmxdFP+PL8EpMiY1O1F2t1ydy2OzBtZz81sycjM9RieFBg==",
"cpu": [
"arm64"
],
@ -545,9 +545,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.14.tgz",
"integrity": "sha512-9iFvUnxG6FC3An5ogp5jbBfQuUmTTwy8KMB+ZddUoPB3NR1eV+Y9vOh/tfWcenSJbgOKDLgYC5D/b1mHAprsrQ==",
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.18.tgz",
"integrity": "sha512-IkukOQUw7/14VkHp446OkYGCZEHqZg9pTmTdBawlUyz2JwZMSn2VodCl7aFSdGCsU4Cwni8zKA8CCgkCCAELhw==",
"cpu": [
"x64"
],
@ -561,9 +561,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.14.tgz",
"integrity": "sha512-zGJsef9qPivKSH8Vv4F/HiBXBTHZ5Hs3ZjVGo/UIdWPJF8fTL9OVADiRrl34Q7zOZEtGXRwEKLUW1SCQcbDvZA==",
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.18.tgz",
"integrity": "sha512-ATnb6jJaBeXCqrTUawWdoOy7eP9SCI7UMcfXlYIMxX4otKKspLPAEuGA5RaNxlCcj9ObyO0J3YGbtZ6hhD2pjg==",
"cpu": [
"arm"
],
@ -577,9 +577,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.14.tgz",
"integrity": "sha512-AxV3MPsoI7i4B8FXOew3dx3N8y00YoJYvIPfxelw07RegeCEH3aHp2U2DtgbP/NV1ugZMx0TL2Z2DEvocmA51g==",
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.18.tgz",
"integrity": "sha512-poHtH7zL7lEp9K2inY90lGHJABWxURAOgWNeZqrcR5+jwIe7q5KBisysH09Zf/JNF9+6iNns+U0xgWTNJzBuGA==",
"cpu": [
"arm64"
],
@ -593,9 +593,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.14.tgz",
"integrity": "sha512-JDLdNjUj3zPehd4+DrQD8Ltb3B5lD8D05IwePyDWw+uR/YPc7w/TX1FUVci5h3giJnlMCJRvi1IQYV7K1n7KtQ==",
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.18.tgz",
"integrity": "sha512-qnNI1WmcOV7Wz1ZDyK6WrOlzLvJ01rnni8ec950mMHWkLRMP53QvCvhF3S+7gFplWBwWJTOOPPUqJp/PlSxWyQ==",
"cpu": [
"arm64"
],
@ -609,9 +609,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.14.tgz",
"integrity": "sha512-Siy5OvPCLLWmMdx4msnEs8HvEVUEigSn0+3pbLjv78iwzXd0qSBNHUPZyC1xeurVaUbpNDxZTpPRIwpqNE2+Og==",
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.18.tgz",
"integrity": "sha512-x9SCqCLzwtlqtD5At3I1a7Gco+EuXnzrJGoucmkpeQohshHuwa+cskqsXO6u1Dz0jXJEuHbBZB9va1wYYfjgFg==",
"cpu": [
"x64"
],
@ -625,9 +625,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.14.tgz",
"integrity": "sha512-FtEGm9mwtRYQNK43WMtUIadxHs/ja2rnDurB99os0ZoFTGG2IHuht2zD97W0wB8JbqEabT1XwSG9Y5wmN+ciEQ==",
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.18.tgz",
"integrity": "sha512-qtj8iOpMMgKjzxTv+islmEY0JBsbd93nka0gzcTTmGZxKtL5jSUsYQvkxwNPZr5M9NU1fgaR3n1vE6lFmtY0IQ==",
"cpu": [
"x64"
],
@ -641,9 +641,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.14.tgz",
"integrity": "sha512-Jp8KDlfq7Ntt2/BXr0y344cYgB1zf0DaLzDZ1ZJR6rYlAzWYSccLYcxHa97VGnsYhhPspMpmCvHid97oe2hl4A==",
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.18.tgz",
"integrity": "sha512-ltX/Ol9+Qu4SXmISCeuwVgAjSa8nzHTymknpozzVMgjXUoZMoz6lcynfKL1nCh5XLgqh0XNHUKLti5YFF8LrrA==",
"cpu": [
"arm64"
],
@ -657,9 +657,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.14.tgz",
"integrity": "sha512-I+cFsXF0OU0J9J4zdWiQKKLURO5dvCujH9Jr8N0cErdy54l9d4gfIxdctfTF+7FyXtWKLTCkp+oby9BQhkFGWA==",
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.18.tgz",
"integrity": "sha512-RgTcFP3wgyxnQbTCJrlgBJmgpeTXo8t807GU9GxApAXfpLZJ3swJ2GgFUmIJVdLWyffSHF5BEkF3FmF6mtH5AQ==",
"cpu": [
"ia32"
],
@ -673,9 +673,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.14.tgz",
"integrity": "sha512-NNrprQCK6d28mG436jVo2TD+vACHseUECacEBGZ9Ef0qfOIWS1XIt2MisQKG0Oea2VvLFl6tF/V4Lnx/H0Sn3Q==",
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.18.tgz",
"integrity": "sha512-XbZ0wAgzR757+DhQcnv60Y/bK9yuWPhDNRQVFFQVRsowvK3+c6EblyfUSytIidpXgyYFzlprq/9A9ZlO/wvDWw==",
"cpu": [
"x64"
],

View File

@ -4,7 +4,7 @@
"private": true,
"license": "MIT",
"dependencies": {
"@goauthentik/api": "^2024.6.3-1724337552",
"@goauthentik/api": "^2024.6.3-1724414734",
"base64-js": "^1.5.1",
"bootstrap": "^4.6.1",
"formdata-polyfill": "^4.0.10",
@ -20,7 +20,7 @@
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-swc": "^0.3.1",
"@swc/cli": "^0.4.0",
"@swc/core": "^1.7.14",
"@swc/core": "^1.7.18",
"@types/jquery": "^3.5.30",
"rollup": "^4.21.0",
"rollup-plugin-copy": "^3.5.0"

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { parseAPIError } from "@goauthentik/common/errors";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
@ -24,7 +25,6 @@ import {
type TransactionApplicationRequest,
type TransactionApplicationResponse,
ValidationError,
ValidationErrorFromJSON,
} from "@goauthentik/api";
import BasePanel from "../BasePanel";
@ -59,7 +59,7 @@ const runningState: State = {
};
const errorState: State = {
state: "error",
label: msg("Authentik was unable to save this application:"),
label: msg("authentik was unable to save this application:"),
icon: ["fa-times-circle", "pf-m-danger"],
};
@ -133,9 +133,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.catch(async (resolution: any) => {
const errors = (this.errors = ValidationErrorFromJSON(
await resolution.response.json(),
));
const errors = await parseAPIError(resolution);
this.dispatchWizardUpdate({
update: {
...this.wizard,

View File

@ -11,7 +11,10 @@ import {
redirectUriHelp,
subjectModeOptions,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
import { oauth2SourcesProvider } from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-number-input";
@ -263,12 +266,12 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
name="jwksSources"
.errorMessages=${errors?.jwksSources ?? []}
>
<ak-dual-select-provider
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selected=${provider?.jwksSources}
.selector=${makeSourceSelector(provider?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-provider>
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",

View File

@ -1,5 +1,8 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import { oauth2SourcesProvider } from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import {
makeProxyPropertyMappingsSelector,
proxyPropertyMappingsProvider,
@ -11,7 +14,6 @@ import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
@ -228,12 +230,12 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
name="jwksSources"
.errorMessages=${errors?.jwksSources ?? []}
>
<ak-dual-select-provider
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selected=${this.instance?.jwksSources}
.selector=${makeSourceSelector(this.instance?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-provider>
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",

View File

@ -1,5 +1,7 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { severityToLabel } from "@goauthentik/common/labels";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
@ -16,6 +18,7 @@ import {
EventsApi,
Group,
NotificationRule,
NotificationTransport,
PaginatedNotificationTransportList,
SeverityEnum,
} from "@goauthentik/api";
@ -34,6 +37,13 @@ async function eventTransportsProvider(page = 1, search = "") {
};
}
export function makeTransportSelector(instanceTransports: string[] | undefined) {
const localTransports = instanceTransports ? new Set(instanceTransports) : undefined;
return localTransports
? ([pk, _]: DualSelectPair) => localTransports.has(pk)
: ([_0, _1, _2, stage]: DualSelectPair<NotificationTransport>) => stage !== undefined;
}
@customElement("ak-event-rule-form")
export class RuleForm extends ModelForm<NotificationRule, string> {
eventTransports?: PaginatedNotificationTransportList;
@ -114,12 +124,12 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
?required=${true}
name="transports"
>
<ak-dual-select-provider
<ak-dual-select-dynamic-selected
.provider=${eventTransportsProvider}
.selected=${this.instance?.transports}
.selector=${makeTransportSelector(this.instance?.transports)}
available-label="${msg("Available Transports")}"
selected-label="${msg("Selected Transports")}"
></ak-dual-select-provider>
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.",

View File

@ -97,7 +97,8 @@ export class OutpostForm extends ModelForm<Outpost, string> {
embedded = false;
@state()
providers?: DataProvider;
providers: DataProvider = providerProvider(this.type);
defaultConfig?: OutpostDefaultConfig;
async loadInstance(pk: string): Promise<Outpost> {
@ -113,6 +114,7 @@ export class OutpostForm extends ModelForm<Outpost, string> {
this.defaultConfig = await new OutpostsApi(
DEFAULT_CONFIG,
).outpostsInstancesDefaultSettingsRetrieve();
this.providers = providerProvider(this.type);
}
getSuccessMessage(): string {

View File

@ -8,7 +8,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
interface PropertyMapping {
name: string;
expression: string;
expression?: string;
}
export abstract class BasePropertyMappingForm<T extends PropertyMapping> extends ModelForm<

View File

@ -1,14 +1,14 @@
import { BasePropertyMappingForm } from "@goauthentik/admin/property-mappings/BasePropertyMappingForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { customElement } from "lit/decorators.js";
import { NotificationWebhookMapping, PropertymappingsApi } from "@goauthentik/api";
@customElement("ak-property-mapping-notification-form")
export class PropertyMappingNotification extends ModelForm<NotificationWebhookMapping, string> {
export class PropertyMappingNotification extends BasePropertyMappingForm<NotificationWebhookMapping> {
loadInstance(pk: string): Promise<NotificationWebhookMapping> {
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsNotificationRetrieve({
pmUuid: pk,

View File

@ -1,10 +1,10 @@
import { BasePropertyMappingForm } from "@goauthentik/admin/property-mappings/BasePropertyMappingForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { docLink } from "@goauthentik/common/global";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
import type { RadioOption } from "@goauthentik/elements/forms/Radio";
@ -33,21 +33,13 @@ export const staticSettingOptions: RadioOption<string | undefined>[] = [
];
@customElement("ak-property-mapping-provider-rac-form")
export class PropertyMappingProviderRACForm extends ModelForm<RACPropertyMapping, string> {
export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACPropertyMapping> {
loadInstance(pk: string): Promise<RACPropertyMapping> {
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsProviderRacRetrieve({
pmUuid: pk,
});
}
getSuccessMessage(): string {
if (this.instance) {
return msg("Successfully updated mapping.");
} else {
return msg("Successfully created mapping.");
}
}
async send(data: RACPropertyMapping): Promise<RACPropertyMapping> {
if (this.instance) {
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsProviderRacUpdate({

View File

@ -10,7 +10,7 @@ import { LDAPSourcePropertyMapping, PropertymappingsApi } from "@goauthentik/api
@customElement("ak-property-mapping-source-ldap-form")
export class PropertyMappingSourceLDAPForm extends BasePropertyMappingForm<LDAPSourcePropertyMapping> {
docLink(): string {
return "/docs/sources/property-mappings/expression?utm_source=authentik";
return "/docs/sources/property-mappings/expressions?utm_source=authentik";
}
loadInstance(pk: string): Promise<LDAPSourcePropertyMapping> {

View File

@ -10,7 +10,7 @@ import { OAuthSourcePropertyMapping, PropertymappingsApi } from "@goauthentik/ap
@customElement("ak-property-mapping-source-oauth-form")
export class PropertyMappingSourceOAuthForm extends BasePropertyMappingForm<OAuthSourcePropertyMapping> {
docLink(): string {
return "/docs/sources/property-mappings/expression?utm_source=authentik";
return "/docs/sources/property-mappings/expressions?utm_source=authentik";
}
loadInstance(pk: string): Promise<OAuthSourcePropertyMapping> {

View File

@ -10,7 +10,7 @@ import { PlexSourcePropertyMapping, PropertymappingsApi } from "@goauthentik/api
@customElement("ak-property-mapping-source-plex-form")
export class PropertyMappingSourcePlexForm extends BasePropertyMappingForm<PlexSourcePropertyMapping> {
docLink(): string {
return "/docs/sources/property-mappings/expression?utm_source=authentik";
return "/docs/sources/property-mappings/expressions?utm_source=authentik";
}
loadInstance(pk: string): Promise<PlexSourcePropertyMapping> {

View File

@ -10,7 +10,7 @@ import { PropertymappingsApi, SAMLSourcePropertyMapping } from "@goauthentik/api
@customElement("ak-property-mapping-source-saml-form")
export class PropertyMappingSourceSAMLForm extends BasePropertyMappingForm<SAMLSourcePropertyMapping> {
docLink(): string {
return "/docs/sources/property-mappings/expression?utm_source=authentik";
return "/docs/sources/property-mappings/expressions?utm_source=authentik";
}
loadInstance(pk: string): Promise<SAMLSourcePropertyMapping> {

View File

@ -10,7 +10,7 @@ import { PropertymappingsApi, SCIMSourcePropertyMapping } from "@goauthentik/api
@customElement("ak-property-mapping-source-scim-form")
export class PropertyMappingSourceSCIMForm extends BasePropertyMappingForm<SCIMSourcePropertyMapping> {
docLink(): string {
return "/docs/sources/property-mappings/expression?utm_source=authentik";
return "/docs/sources/property-mappings/expressions?utm_source=authentik";
}
loadInstance(pk: string): Promise<SCIMSourcePropertyMapping> {

View File

@ -61,7 +61,9 @@ export class PolicyTestForm extends Form<PropertyMappingTestRequest> {
</ak-codemirror>`
: html` <div class="pf-c-form__group-label">
<div class="c-form__horizontal-group">
<span class="pf-c-form__label-text">${this.result?.result}</span>
<span class="pf-c-form__label-text">
<pre>${this.result?.result}</pre>
</span>
</div>
</div>`}
</ak-form-element-horizontal>`;

View File

@ -27,7 +27,7 @@ export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProvi
renderToolbar(): TemplateResult {
return html`<ak-forms-modal cancelText=${msg("Close")} ?closeAfterSuccessfulSubmit=${false}>
<span slot="submit">${msg("Sync")}</span>
<span slot="header">${msg("Sync User")}</span>
<span slot="header">${msg("Sync Group")}</span>
<ak-sync-object-form
.provider=${this.providerId}
model=${SyncObjectModelEnum.Group}

View File

@ -24,7 +24,7 @@ export class MicrosoftEntraProviderGroupList extends Table<MicrosoftEntraProvide
renderToolbar(): TemplateResult {
return html`<ak-forms-modal cancelText=${msg("Close")} ?closeAfterSuccessfulSubmit=${false}>
<span slot="submit">${msg("Sync")}</span>
<span slot="header">${msg("Sync User")}</span>
<span slot="header">${msg("Sync Group")}</span>
<ak-sync-object-form
.provider=${this.providerId}
model=${SyncObjectModelEnum.Group}

View File

@ -3,6 +3,12 @@ import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import { PropertymappingsApi, ScopeMapping } from "@goauthentik/api";
export const defaultScopes = [
"goauthentik.io/providers/oauth2/scope-openid",
"goauthentik.io/providers/oauth2/scope-email",
"goauthentik.io/providers/oauth2/scope-profile",
];
export async function oauth2PropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
@ -23,6 +29,5 @@ export function makeOAuth2PropertyMappingsSelector(instanceMappings: string[] |
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, scope]: DualSelectPair<ScopeMapping>) =>
scope?.managed?.startsWith("goauthentik.io/providers/oauth2/scope-") &&
scope?.managed !== "goauthentik.io/providers/oauth2/scope-offline_access";
scope?.managed && defaultScopes.includes(scope?.managed);
}

View File

@ -32,7 +32,7 @@ import {
makeOAuth2PropertyMappingsSelector,
oauth2PropertyMappingsProvider,
} from "./OAuth2PropertyMappings.js";
import { oauth2SourcesProvider } from "./OAuth2Sources.js";
import { makeSourceSelector, oauth2SourcesProvider } from "./OAuth2Sources.js";
export const clientTypeOptions = [
{
@ -52,12 +52,6 @@ export const clientTypeOptions = [
},
];
export const defaultScopes = [
"goauthentik.io/providers/oauth2/scope-openid",
"goauthentik.io/providers/oauth2/scope-email",
"goauthentik.io/providers/oauth2/scope-profile",
];
export const subjectModeOptions = [
{
label: msg("Based on the User's hashed ID"),
@ -335,12 +329,12 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
>
<ak-dual-select-provider
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selected=${provider?.jwksSources}
.selector=${makeSourceSelector(provider?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-provider>
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",

View File

@ -1,6 +1,7 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import { SourcesApi } from "@goauthentik/api";
import { OAuthSource, SourcesApi } from "@goauthentik/api";
export async function oauth2SourcesProvider(page = 1, search = "") {
const oauthSources = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthList({
@ -19,3 +20,11 @@ export async function oauth2SourcesProvider(page = 1, search = "") {
]),
};
}
export function makeSourceSelector(instanceSources: string[] | undefined) {
const localSources = instanceSources ? new Set(instanceSources) : undefined;
return localSources
? ([pk, _]: DualSelectPair) => localSources.has(pk)
: ([_0, _1, _2, prompt]: DualSelectPair<OAuthSource>) => prompt !== undefined;
}

View File

@ -1,12 +1,14 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import { oauth2SourcesProvider } from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@ -403,12 +405,12 @@ ${this.instance?.skipPathRegex}</textarea
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
>
<ak-dual-select-provider
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selected=${this.instance?.jwksSources}
.selector=${makeSourceSelector(this.instance?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-provider>
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",

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