Compare commits

..

61 Commits

Author SHA1 Message Date
3925f5a208 release: 2023.5.6 2023-08-29 19:36:52 +02:00
6add4a62b9 include cure53 report
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-29 19:35:50 +02:00
54d5aa20ba security: fix CVE-2023-39522 (#6665)
* stages/email: don't disclose whether a user exists or not when recovering

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

* update website

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	website/docs/releases/2023/v2023.5.md
#	website/docs/releases/2023/v2023.6.md
2023-08-29 19:08:47 +02:00
b99ac01228 release: 2023.5.5 2023-07-06 18:15:56 +02:00
15026748d1 security: fix CVE-2023-36456
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	website/sidebars.js
2023-07-06 18:15:46 +02:00
2739376a2a release: 2023.5.4 2023-06-22 21:45:33 +02:00
152121175b bump web api client
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-22 21:33:02 +02:00
1d57a258f3 ATH-01-012: escape quotation marks
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:48:08 +02:00
f15cac39c8 ATH-01-014: save authenticator validation state in flow context
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

bugfixes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:48:05 +02:00
ce77d82b24 ATH-01-010: rework
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:48:03 +02:00
c3fe57197d ATH-01-009: migrate impersonation to use API
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	authentik/core/urls.py
#	web/src/admin/AdminInterface.ts
#	web/src/admin/users/RelatedUserList.ts
#	web/src/admin/users/UserListPage.ts
#	web/src/admin/users/UserViewPage.ts
#	web/src/user/UserInterface.ts

# Conflicts:
#	authentik/core/urls.py
2023-06-19 13:47:53 +02:00
267938d435 ATH-01-005: use hmac.compare_digest for secret_key authentication
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:47:11 +02:00
6a7c2e0662 ATH-01-003 / ATH-01-012: disable htmlLabels in mermaid
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:47:09 +02:00
5336afb1b4 ATH-01-004: remove env from admin system endpoint
this endpoint already required admin access, but for debugging the env variables are used very little

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:47:06 +02:00
9bb44055a3 ATH-01-008: fix web forms not submitting correctly when pressing enter
When submitting some forms with the Enter key instead of clicking "Confirm"/etc, the form would not get submitted correctly

This would in the worst case is when setting a user's password, where the new password can end up in the URL, but the password was not actually saved to the user.

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

# Conflicts:
#	web/src/admin/applications/ApplicationCheckAccessForm.ts
#	web/src/admin/crypto/CertificateGenerateForm.ts
#	web/src/admin/flows/FlowImportForm.ts
#	web/src/admin/groups/RelatedGroupList.ts
#	web/src/admin/policies/PolicyTestForm.ts
#	web/src/admin/property-mappings/PropertyMappingTestForm.ts
#	web/src/admin/providers/saml/SAMLProviderImportForm.ts
#	web/src/admin/users/RelatedUserList.ts
#	web/src/admin/users/ServiceAccountForm.ts
#	web/src/admin/users/UserPasswordForm.ts
#	web/src/admin/users/UserResetEmailForm.ts

# Conflicts:
#	web/src/admin/property-mappings/PropertyMappingTestForm.ts
2023-06-19 13:46:52 +02:00
143663d293 ATH-01-010: fix missing user filter for webauthn device
This prevents an attack that is only possible when an attacker can intercept HTTP traffic and in the case of HTTPS decrypt it.
2023-06-19 13:46:16 +02:00
bd54d034e1 ATH-01-001: resolve path and check start before loading blueprints
This is even less of an issue since 411ef239f6, since with that commit we only allow files that the listing returns

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:46:13 +02:00
be85eecac5 release: 2023.5.3 2023-06-01 19:35:13 +02:00
24385c9c68 ci: build outpost binaries statically linked (#5823) 2023-05-31 16:58:10 +02:00
e141a11475 blueprints: fix API validation with OCI blueprint path (#5822)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-31 14:52:12 +02:00
b055adec2a ci: replace github bot account with github app (#5819)
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-05-31 14:52:09 +02:00
772acb10d6 providers/ldap: fix LDAP Outpost application selection (#5812)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-31 14:51:46 +02:00
a7bf963409 stages/deny: fix typos (#5800)
* Fix typo in stage.py

Fix typo in "Cancells the current flow"

Signed-off-by: rlew-is <96594816+rlew-is@users.noreply.github.com>

* Fix typo in models.py

Fix typo in "Cancells the current flow"

Signed-off-by: rlew-is <96594816+rlew-is@users.noreply.github.com>

---------

Signed-off-by: rlew-is <96594816+rlew-is@users.noreply.github.com>
2023-05-30 10:54:24 +02:00
317afc932a web/flows: fix RedirectStage not detecting absolute URLs correctly (#5781)
* web: getURL() method in RedirectStage.ts now actually detects URLs (#5732)

Signed-off-by: Saeverix <1863379+Saeverix@users.noreply.github.com>

* use native API to build full URL

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

---------

Signed-off-by: Saeverix <1863379+Saeverix@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-05-30 10:54:12 +02:00
5e5a74eebf release: 2023.5.2 2023-05-26 23:54:12 +02:00
fa87519536 core: bump coverage from 7.2.5 to 7.2.6 (#5738)
* core: bump coverage from 7.2.5 to 7.2.6

Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.2.5 to 7.2.6.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.2.5...7.2.6)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* use tagged oauth1 server

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>
2023-05-26 23:33:05 +02:00
0deaf25b1f web/user: fix MFA enroll dropdown broken when password stage has no configuration flow (#5744)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-24 21:52:21 +02:00
47d5fc26cc events: fix ak_create_event using wrong request for event creation (#5731)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-24 21:52:14 +02:00
9a996e7176 outposts: fix missing radius outpost controller (#5730)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-24 21:52:11 +02:00
554a26442d blueprints: support custom ports for OCI blueprints (#5727)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-24 21:52:07 +02:00
573517bf0a lib: add tests for ak_create_event (#5710)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	locale/en/LC_MESSAGES/django.po
2023-05-24 21:51:52 +02:00
2cd68dfa87 blueprints: fix check for file path not being run on worker (#5703) 2023-05-24 21:51:30 +02:00
8029a13be1 core: make groups field for user optional (#5702)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-24 21:51:23 +02:00
6900ffffd8 release: 2023.5.1 2023-05-18 21:33:38 +02:00
873aaf85f9 website/docs: prepare 2023.5.1 release notes (#5679)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-18 20:34:33 +02:00
9c69f67778 sources/ldap: log full exception when user password set fails (#5678)
* sources/ldap: log full exception when user password set fails

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

* Update authentik/sources/ldap/auth.py

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens L. <jens@beryju.org>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2023-05-18 19:00:17 +02:00
6cf7a72831 web/flows: improve UI for TOTP code input (#5676)
* web/flows: improve UI for TOTP code input

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

* update locale

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

* update phrasing

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-18 18:41:53 +02:00
Sem
7e3b325929 website/integrations: Updated AWS docs for the new IAM Center and SCIM (#5643)
* Updated AWS Integration docs

Updated the AWS Integration docs to match the new IAM Centre's method.
This includes SCIM.

Signed-off-by: Sem <86064734+justSem@users.noreply.github.com>

* website/docs: Updated AWS Docs

* website/docs: AWS - Updated AWS docs to allow for both methods

* format, cleanup mapping, follow guidelines

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

* Apply suggestions from code review

Looks good to me!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Sem <86064734+justSem@users.noreply.github.com>

* final formatting pass

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

---------

Signed-off-by: Sem <86064734+justSem@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2023-05-18 11:45:26 +02:00
b916b612c7 core: bump github.com/sirupsen/logrus from 1.9.1 to 1.9.2 (#5670)
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.9.1 to 1.9.2.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.9.1...v1.9.2)

---
updated-dependencies:
- dependency-name: github.com/sirupsen/logrus
  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>
2023-05-18 11:05:42 +02:00
b7c5fc3f1e ci: bump helm/kind-action from 1.6.0 to 1.7.0 (#5667)
Bumps [helm/kind-action](https://github.com/helm/kind-action) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/helm/kind-action/releases)
- [Commits](https://github.com/helm/kind-action/compare/v1.6.0...v1.7.0)

---
updated-dependencies:
- dependency-name: helm/kind-action
  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>
2023-05-18 10:51:48 +02:00
a3ac5ec183 web: bump tslib from 2.5.0 to 2.5.1 in /web (#5668)
Bumps [tslib](https://github.com/Microsoft/tslib) from 2.5.0 to 2.5.1.
- [Release notes](https://github.com/Microsoft/tslib/releases)
- [Commits](https://github.com/Microsoft/tslib/compare/2.5.0...2.5.1)

---
updated-dependencies:
- dependency-name: tslib
  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>
2023-05-18 10:50:26 +02:00
d30379ba93 core: bump sentry-sdk from 1.23.0 to 1.23.1 (#5669)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 1.23.0 to 1.23.1.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/1.23.0...1.23.1)

---
updated-dependencies:
- dependency-name: sentry-sdk
  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>
2023-05-18 10:49:36 +02:00
12815526c1 core: bump goauthentik.io/api/v3 from 3.2023050.1 to 3.2023050.2 (#5671)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023050.1 to 3.2023050.2.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023050.1...v3.2023050.2)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/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>
2023-05-18 10:48:46 +02:00
ed2f0a2d5e website/docs: edits to full dev env (#5636)
* edits to install full dev env

* remove json files

* Update website/developer-docs/setup/full-dev-environment.md

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/developer-docs/setup/full-dev-environment.md

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/developer-docs/setup/full-dev-environment.md

Co-authored-by: Jens L. <jens@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/developer-docs/setup/full-dev-environment.md

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/developer-docs/setup/full-dev-environment.md

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* few tweaks per review

---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana Berry <tana@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2023-05-17 15:44:47 -05:00
536d776d02 website/blog: flex-hours-blog-draft-for-review (#5598)
* blog-draft-for-review

* tweaks

* delete swp file

* further tweaks

* quote marks for title

* edits

* linter

---------

Co-authored-by: Tana Berry <tana@goauthentik.io>
2023-05-17 20:21:11 +02:00
f70d6432e7 web: bump API Client version (#5664) 2023-05-17 16:33:55 +02:00
cc08bfb18b web: bump @lingui/core from 4.1.0 to 4.1.2 in /web (#5658)
* web: bump @lingui/core from 4.1.0 to 4.1.2 in /web

Bumps [@lingui/core](https://github.com/lingui/js-lingui) from 4.1.0 to 4.1.2.
- [Release notes](https://github.com/lingui/js-lingui/releases)
- [Changelog](https://github.com/lingui/js-lingui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lingui/js-lingui/compare/v4.1.0...v4.1.2)

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

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

* update all of lingui

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>
2023-05-17 16:20:59 +02:00
79dcc30778 providers/radius: add warning message when radius provider is not used with outpost (#5656)
* providers/radius: add warning message when radius provider is not used with outpost

same message as Proxy and LDAP provider have

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

* format

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-17 16:19:33 +02:00
68a1bcf233 providers/SCIM: improve backchannel signalling (#5657)
* providers/scim: add warning when provider is not used as backchannel provider

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

* providers/scim: don't sync SCIM provider that isn't used as backchannel at all

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

* fix tests

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-17 16:19:18 +02:00
cd7de4c0b9 sources/ldap: improve error message (#5653)
* sources/ldap: improve ldap password change error message

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

* stages/user_write: handle validation error when updating user

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-17 15:26:46 +02:00
3195a75b9a web/admin: fix radius provider page (#5651)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-17 10:56:24 +02:00
886d7832df ci: bump helm/kind-action from 1.5.0 to 1.6.0 (#5646)
Bumps [helm/kind-action](https://github.com/helm/kind-action) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/helm/kind-action/releases)
- [Commits](https://github.com/helm/kind-action/compare/v1.5.0...v1.6.0)

---
updated-dependencies:
- dependency-name: helm/kind-action
  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>
2023-05-17 10:30:34 +02:00
a3595a36d2 core: bump github.com/sirupsen/logrus from 1.9.0 to 1.9.1 (#5648)
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.9.0 to 1.9.1.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.9.0...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/sirupsen/logrus
  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>
2023-05-17 10:29:49 +02:00
28ac00798c core: bump goauthentik.io/api/v3 from 3.2023041.12 to 3.2023050.1 (#5647)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023041.12 to 3.2023050.1.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023041.12...v3.2023050.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>
2023-05-17 10:29:32 +02:00
f4b0d6e85c providers/scim: default to None for fields instead of empty list (#5642)
* providers/scim: default to None for fields instead of empty list

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

* make name of delete_none_keys clearer

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

* fix

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-17 00:25:28 +02:00
daa3c91afc web/flows: fix authenticator_validate device select not sent to backend (#5638) 2023-05-16 22:55:59 +02:00
5eba598584 web/flows: update flow background (#5639)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-16 22:18:13 +02:00
a6b16ecc68 lib: fix fallback_names migration not working when multiple objects n… (#5637)
lib: fix fallback_names migration not working when multiple objects need to be renamed

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-16 22:17:56 +02:00
a41924939b web: bump API Client version (#5633) 2023-05-16 14:56:15 +02:00
0afd3b121e Merge branch 'version-2023.5' 2023-05-16 14:53:18 +02:00
a58374f065 website/docs: add 2023.5 to release sidebar (#5631)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-16 14:12:30 +02:00
129 changed files with 2390 additions and 1742 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2023.5.0 current_version = 2023.5.6
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)

View File

@ -112,7 +112,7 @@ jobs:
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: Create k8s Kind Cluster - name: Create k8s Kind Cluster
uses: helm/kind-action@v1.5.0 uses: helm/kind-action@v1.7.0
- name: run integration - name: run integration
run: | run: |
poetry run coverage run manage.py test tests/integration poetry run coverage run manage.py test tests/integration

View File

@ -135,4 +135,5 @@ jobs:
set -x set -x
export GOOS=${{ matrix.goos }} export GOOS=${{ matrix.goos }}
export GOARCH=${{ matrix.goarch }} export GOARCH=${{ matrix.goarch }}
export CGO_ENABLED=0
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }} go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}

View File

@ -10,6 +10,11 @@ jobs:
name: Delete old unused container images name: Delete old unused container images
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Delete 'dev' containers older than a week - name: Delete 'dev' containers older than a week
uses: snok/container-retention-policy@v2 uses: snok/container-retention-policy@v2
with: with:
@ -18,5 +23,5 @@ jobs:
account-type: org account-type: org
org-name: goauthentik org-name: goauthentik
untagged-only: false untagged-only: false
token: ${{ secrets.BOT_GITHUB_TOKEN }} token: ${{ steps.generate_token.outputs.token }}
skip-tags: gh-next,gh-main skip-tags: gh-next,gh-main

View File

@ -123,6 +123,7 @@ jobs:
set -x set -x
export GOOS=${{ matrix.goos }} export GOOS=${{ matrix.goos }}
export GOARCH=${{ matrix.goarch }} export GOARCH=${{ matrix.goarch }}
export CGO_ENABLED=0
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }} go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
- name: Upload binaries to release - name: Upload binaries to release
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2

View File

@ -22,18 +22,23 @@ jobs:
docker-compose up --no-start docker-compose up --no-start
docker-compose start postgresql redis docker-compose start postgresql redis
docker-compose run -u root server test-all docker-compose run -u root server test-all
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Extract version number - name: Extract version number
id: get_version id: get_version
uses: actions/github-script@v6 uses: actions/github-script@v6
with: with:
github-token: ${{ secrets.BOT_GITHUB_TOKEN }} github-token: ${{ steps.generate_token.outputs.token }}
script: | script: |
return context.payload.ref.replace(/\/refs\/tags\/version\//, ''); return context.payload.ref.replace(/\/refs\/tags\/version\//, '');
- name: Create Release - name: Create Release
id: create_release id: create_release
uses: actions/create-release@v1.1.4 uses: actions/create-release@v1.1.4
env: env:
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
with: with:
tag_name: ${{ github.ref }} tag_name: ${{ github.ref }}
release_name: Release ${{ steps.get_version.outputs.result }} release_name: Release ${{ steps.get_version.outputs.result }}

View File

@ -15,9 +15,14 @@ jobs:
compile: compile:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
token: ${{ secrets.BOT_GITHUB_TOKEN }} token: ${{ steps.generate_token.outputs.token }}
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: run compile - name: run compile
@ -26,7 +31,7 @@ jobs:
uses: peter-evans/create-pull-request@v5 uses: peter-evans/create-pull-request@v5
id: cpr id: cpr
with: with:
token: ${{ secrets.BOT_GITHUB_TOKEN }} token: ${{ steps.generate_token.outputs.token }}
branch: compile-backend-translation branch: compile-backend-translation
commit-message: "core: compile backend translations" commit-message: "core: compile backend translations"
title: "core: compile backend translations" title: "core: compile backend translations"

View File

@ -9,9 +9,14 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
token: ${{ secrets.BOT_GITHUB_TOKEN }} token: ${{ steps.generate_token.outputs.token }}
- uses: actions/setup-node@v3.6.0 - uses: actions/setup-node@v3.6.0
with: with:
node-version: "20" node-version: "20"
@ -33,7 +38,7 @@ jobs:
- uses: peter-evans/create-pull-request@v5 - uses: peter-evans/create-pull-request@v5
id: cpr id: cpr
with: with:
token: ${{ secrets.BOT_GITHUB_TOKEN }} token: ${{ steps.generate_token.outputs.token }}
branch: update-web-api-client branch: update-web-api-client
commit-message: "web: bump API Client version" commit-message: "web: bump API Client version"
title: "web: bump API Client version" title: "web: bump API Client version"
@ -44,6 +49,6 @@ jobs:
author: authentik bot <github-bot@goauthentik.io> author: authentik bot <github-bot@goauthentik.io>
- uses: peter-evans/enable-pull-request-automerge@v3 - uses: peter-evans/enable-pull-request-automerge@v3
with: with:
token: ${{ secrets.BOT_GITHUB_TOKEN }} token: ${{ steps.generate_token.outputs.token }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
merge-method: squash merge-method: squash

View File

@ -1,10 +1,11 @@
{ {
"recommendations": [ "recommendations": [
"EditorConfig.EditorConfig",
"bashmish.es6-string-css", "bashmish.es6-string-css",
"bpruitt-goddard.mermaid-markdown-syntax-highlighting", "bpruitt-goddard.mermaid-markdown-syntax-highlighting",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"github.vscode-github-actions",
"golang.go", "golang.go",
"Gruntfuggly.todo-tree", "Gruntfuggly.todo-tree",
"mechatroner.rainbow-csv", "mechatroner.rainbow-csv",
@ -15,6 +16,6 @@
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"redhat.vscode-yaml", "redhat.vscode-yaml",
"Tobermory.es6-string-html", "Tobermory.es6-string-html",
"unifiedjs.vscode-mdx" "unifiedjs.vscode-mdx",
] ]
} }

View File

@ -48,5 +48,10 @@
"ignoreCase": false "ignoreCase": false
} }
], ],
"go.testFlags": ["-count=1"] "go.testFlags": [
"-count=1"
],
"github-actions.workflows.pinned.workflows": [
".github/workflows/ci-main.yml"
]
} }

View File

@ -6,8 +6,8 @@ Authentik takes security very seriously. We follow the rules of [responsible dis
| Version | Supported | | Version | Supported |
| --------- | ------------------ | | --------- | ------------------ |
| 2023.2.x | :white_check_mark: | | 2023.4.x | :white_check_mark: |
| 2023.3.x | :white_check_mark: | | 2023.5.x | :white_check_mark: |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
from typing import Optional from typing import Optional
__version__ = "2023.5.0" __version__ = "2023.5.6"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -1,5 +1,4 @@
"""authentik administration overview""" """authentik administration overview"""
import os
import platform import platform
from datetime import datetime from datetime import datetime
from sys import version as python_version from sys import version as python_version
@ -34,7 +33,6 @@ class RuntimeDict(TypedDict):
class SystemSerializer(PassiveSerializer): class SystemSerializer(PassiveSerializer):
"""Get system information.""" """Get system information."""
env = SerializerMethodField()
http_headers = SerializerMethodField() http_headers = SerializerMethodField()
http_host = SerializerMethodField() http_host = SerializerMethodField()
http_is_secure = SerializerMethodField() http_is_secure = SerializerMethodField()
@ -43,10 +41,6 @@ class SystemSerializer(PassiveSerializer):
server_time = SerializerMethodField() server_time = SerializerMethodField()
embedded_outpost_host = SerializerMethodField() embedded_outpost_host = SerializerMethodField()
def get_env(self, request: Request) -> dict[str, str]:
"""Get Environment"""
return os.environ.copy()
def get_http_headers(self, request: Request) -> dict[str, str]: def get_http_headers(self, request: Request) -> dict[str, str]:
"""Get HTTP Request headers""" """Get HTTP Request headers"""
headers = {} headers = {}

View File

@ -1,4 +1,5 @@
"""API Authentication""" """API Authentication"""
from hmac import compare_digest
from typing import Any, Optional from typing import Any, Optional
from django.conf import settings from django.conf import settings
@ -78,7 +79,7 @@ def token_secret_key(value: str) -> Optional[User]:
and return the service account for the managed outpost""" and return the service account for the managed outpost"""
from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.apps import MANAGED_OUTPOST
if value != settings.SECRET_KEY: if not compare_digest(value, settings.SECRET_KEY):
return None return None
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST) outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
if not outposts: if not outposts:

View File

@ -11,8 +11,9 @@ from rest_framework.serializers import ListSerializer, ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
@ -35,11 +36,12 @@ class BlueprintInstanceSerializer(ModelSerializer):
"""Info about a single blueprint instance file""" """Info about a single blueprint instance file"""
def validate_path(self, path: str) -> str: def validate_path(self, path: str) -> str:
"""Ensure the path specified is retrievable""" """Ensure the path (if set) specified is retrievable"""
try: if path == "" or path.startswith(OCI_PREFIX):
BlueprintInstance(path=path).retrieve() return path
except BlueprintRetrievalFailed as exc: files: list[dict] = blueprints_find_dict.delay().get()
raise ValidationError(exc) from exc if path not in [file["path"] for file in files]:
raise ValidationError(_("Blueprint file does not exist"))
return path return path
def validate_content(self, content: str) -> str: def validate_content(self, content: str) -> str:

View File

@ -45,7 +45,7 @@ def check_blueprint_v1_file(BlueprintInstance: type, path: Path):
enabled=True, enabled=True,
managed_models=[], managed_models=[],
last_applied_hash="", last_applied_hash="",
metadata=metadata, metadata=metadata or {},
) )
instance.save() instance.save()

View File

@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from structlog import get_logger from structlog import get_logger
from authentik.blueprints.v1.oci import BlueprintOCIClient, OCIException from authentik.blueprints.v1.oci import OCI_PREFIX, BlueprintOCIClient, OCIException
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.models import CreatedUpdatedModel, SerializerModel from authentik.lib.models import CreatedUpdatedModel, SerializerModel
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
@ -72,7 +72,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
def retrieve_oci(self) -> str: def retrieve_oci(self) -> str:
"""Get blueprint from an OCI registry""" """Get blueprint from an OCI registry"""
client = BlueprintOCIClient(self.path.replace("oci://", "https://")) client = BlueprintOCIClient(self.path.replace(OCI_PREFIX, "https://"))
try: try:
manifests = client.fetch_manifests() manifests = client.fetch_manifests()
return client.fetch_blobs(manifests) return client.fetch_blobs(manifests)
@ -82,7 +82,10 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
def retrieve_file(self) -> str: def retrieve_file(self) -> str:
"""Get blueprint from path""" """Get blueprint from path"""
try: try:
full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(self.path)) base = Path(CONFIG.y("blueprints_dir"))
full_path = base.joinpath(Path(self.path)).resolve()
if not str(full_path).startswith(str(base.resolve())):
raise BlueprintRetrievalFailed("Invalid blueprint path")
with full_path.open("r", encoding="utf-8") as _file: with full_path.open("r", encoding="utf-8") as _file:
return _file.read() return _file.read()
except (IOError, OSError) as exc: except (IOError, OSError) as exc:
@ -90,7 +93,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
def retrieve(self) -> str: def retrieve(self) -> str:
"""Retrieve blueprint contents""" """Retrieve blueprint contents"""
if self.path.startswith("oci://"): if self.path.startswith(OCI_PREFIX):
return self.retrieve_oci() return self.retrieve_oci()
if self.path != "": if self.path != "":
return self.retrieve_file() return self.retrieve_file()

View File

@ -1,34 +1,15 @@
"""authentik managed models tests""" """authentik managed models tests"""
from typing import Callable, Type
from django.apps import apps
from django.test import TestCase from django.test import TestCase
from authentik.blueprints.v1.importer import is_model_allowed from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed
from authentik.lib.models import SerializerModel from authentik.lib.generators import generate_id
class TestModels(TestCase): class TestModels(TestCase):
"""Test Models""" """Test Models"""
def test_retrieve_file(self):
def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable: """Test retrieve_file"""
"""Test serializer""" instance = BlueprintInstance.objects.create(name=generate_id(), path="../etc/hosts")
with self.assertRaises(BlueprintRetrievalFailed):
def tester(self: TestModels): instance.retrieve()
if test_model._meta.abstract: # pragma: no cover
return
model_class = test_model()
self.assertTrue(isinstance(model_class, SerializerModel))
self.assertIsNotNone(model_class.serializer)
return tester
for app in apps.get_app_configs():
if not app.label.startswith("authentik"):
continue
for model in app.get_models():
if not is_model_allowed(model):
continue
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))

View File

@ -32,6 +32,29 @@ class TestBlueprintOCI(TransactionTestCase):
"foo", "foo",
) )
def test_successful_port(self):
"""Successful retrieval with custom port"""
with Mocker() as mocker:
mocker.get(
"https://ghcr.io:1234/v2/goauthentik/blueprints/test/manifests/latest",
json={
"layers": [
{
"mediaType": OCI_MEDIA_TYPE,
"digest": "foo",
}
]
},
)
mocker.get("https://ghcr.io:1234/v2/goauthentik/blueprints/test/blobs/foo", text="foo")
self.assertEqual(
BlueprintInstance(
path="oci://ghcr.io:1234/goauthentik/blueprints/test:latest"
).retrieve(),
"foo",
)
def test_manifests_error(self): def test_manifests_error(self):
"""Test manifests request erroring""" """Test manifests request erroring"""
with Mocker() as mocker: with Mocker() as mocker:

View File

@ -0,0 +1,34 @@
"""authentik managed models tests"""
from typing import Callable, Type
from django.apps import apps
from django.test import TestCase
from authentik.blueprints.v1.importer import is_model_allowed
from authentik.lib.models import SerializerModel
class TestModels(TestCase):
"""Test Models"""
def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable:
"""Test serializer"""
def tester(self: TestModels):
if test_model._meta.abstract: # pragma: no cover
return
model_class = test_model()
self.assertTrue(isinstance(model_class, SerializerModel))
self.assertIsNotNone(model_class.serializer)
return tester
for app in apps.get_app_configs():
if not app.label.startswith("authentik"):
continue
for model in app.get_models():
if not is_model_allowed(model):
continue
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))

View File

@ -44,6 +44,14 @@ class TestBlueprintsV1API(APITestCase):
), ),
) )
def test_api_oci(self):
"""Test validation with OCI path"""
res = self.client.post(
reverse("authentik_api:blueprintinstance-list"),
data={"name": "foo", "path": "oci://foo/bar"},
)
self.assertEqual(res.status_code, 201)
def test_api_blank(self): def test_api_blank(self):
"""Test blank""" """Test blank"""
res = self.client.post( res = self.client.post(

View File

@ -19,6 +19,7 @@ from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.http import authentik_user_agent from authentik.lib.utils.http import authentik_user_agent
OCI_MEDIA_TYPE = "application/vnd.goauthentik.blueprint.v1+yaml" OCI_MEDIA_TYPE = "application/vnd.goauthentik.blueprint.v1+yaml"
OCI_PREFIX = "oci://"
class OCIException(SentryIgnoredException): class OCIException(SentryIgnoredException):
@ -39,11 +40,16 @@ class BlueprintOCIClient:
self.logger = get_logger().bind(url=self.sanitized_url) self.logger = get_logger().bind(url=self.sanitized_url)
self.ref = "latest" self.ref = "latest"
# Remove the leading slash of the path to convert it to an image name
path = self.url.path[1:] path = self.url.path[1:]
if ":" in self.url.path: if ":" in path:
# if there's a colon in the path, use everything after it as a ref
path, _, self.ref = path.partition(":") path, _, self.ref = path.partition(":")
base_url = f"https://{self.url.hostname}"
if self.url.port:
base_url += f":{self.url.port}"
self.client = NewClient( self.client = NewClient(
f"https://{self.url.hostname}", base_url,
WithUserAgent(authentik_user_agent()), WithUserAgent(authentik_user_agent()),
WithUsernamePassword(self.url.username, self.url.password), WithUsernamePassword(self.url.username, self.url.password),
WithDefaultName(path), WithDefaultName(path),

View File

@ -28,6 +28,7 @@ from authentik.blueprints.models import (
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata, EntryInvalidError from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata, EntryInvalidError
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_INSTANTIATE from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_INSTANTIATE
from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.events.monitored_tasks import ( from authentik.events.monitored_tasks import (
MonitoredTask, MonitoredTask,
TaskResult, TaskResult,
@ -228,7 +229,7 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str):
def clear_failed_blueprints(): def clear_failed_blueprints():
"""Remove blueprints which couldn't be fetched""" """Remove blueprints which couldn't be fetched"""
# Exclude OCI blueprints as those might be temporarily unavailable # Exclude OCI blueprints as those might be temporarily unavailable
for blueprint in BlueprintInstance.objects.exclude(path__startswith="oci://"): for blueprint in BlueprintInstance.objects.exclude(path__startswith=OCI_PREFIX):
try: try:
blueprint.retrieve() blueprint.retrieve()
except BlueprintRetrievalFailed: except BlueprintRetrievalFailed:

View File

@ -1,4 +1,6 @@
"""Provider API Views""" """Provider API Views"""
from django.db.models import QuerySet
from django.db.models.query import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_filters.filters import BooleanFilter from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet from django_filters.filterset import FilterSet
@ -56,17 +58,22 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
class ProviderFilter(FilterSet): class ProviderFilter(FilterSet):
"""Filter for groups""" """Filter for providers"""
application__isnull = BooleanFilter( application__isnull = BooleanFilter(method="filter_application__isnull")
field_name="application",
lookup_expr="isnull",
)
backchannel_only = BooleanFilter( backchannel_only = BooleanFilter(
method="filter_backchannel_only", method="filter_backchannel_only",
) )
def filter_backchannel_only(self, queryset, name, value): def filter_application__isnull(self, queryset: QuerySet, name, value):
"""Only return providers that are neither assigned to application,
both as provider or application provider"""
return queryset.filter(
Q(backchannel_application__isnull=value, is_backchannel=True)
| Q(application__isnull=value)
)
def filter_backchannel_only(self, queryset: QuerySet, name, value):
"""Only return backchannel providers""" """Only return backchannel providers"""
return queryset.filter(is_backchannel=value) return queryset.filter(is_backchannel=value)

View File

@ -67,11 +67,12 @@ from authentik.core.models import (
TokenIntents, TokenIntents,
User, User,
) )
from authentik.events.models import EventAction from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowToken from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
from authentik.flows.views.executor import QS_KEY_TOKEN from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.lib.config import CONFIG
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
@ -106,7 +107,7 @@ class UserSerializer(ModelSerializer):
avatar = CharField(read_only=True) avatar = CharField(read_only=True)
attributes = JSONField(validators=[is_dict], required=False) attributes = JSONField(validators=[is_dict], required=False)
groups = PrimaryKeyRelatedField( groups = PrimaryKeyRelatedField(
allow_empty=True, many=True, source="ak_groups", queryset=Group.objects.all() allow_empty=True, many=True, source="ak_groups", queryset=Group.objects.all(), default=list
) )
groups_obj = ListSerializer(child=UserGroupSerializer(), read_only=True, source="ak_groups") groups_obj = ListSerializer(child=UserGroupSerializer(), read_only=True, source="ak_groups")
uid = CharField(read_only=True) uid = CharField(read_only=True)
@ -543,6 +544,58 @@ class UserViewSet(UsedByMixin, ModelViewSet):
send_mails(email_stage, message) send_mails(email_stage, message)
return Response(status=204) return Response(status=204)
@permission_required("authentik_core.impersonate")
@extend_schema(
request=OpenApiTypes.NONE,
responses={
"204": OpenApiResponse(description="Successfully started impersonation"),
"401": OpenApiResponse(description="Access denied"),
},
)
@action(detail=True, methods=["POST"])
def impersonate(self, request: Request, pk: int) -> Response:
"""Impersonate a user"""
if not CONFIG.y_bool("impersonation"):
LOGGER.debug("User attempted to impersonate", user=request.user)
return Response(status=401)
if not request.user.has_perm("impersonate"):
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
return Response(status=401)
user_to_be = self.get_object()
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
return Response(status=201)
@extend_schema(
request=OpenApiTypes.NONE,
responses={
"204": OpenApiResponse(description="Successfully started impersonation"),
},
)
@action(detail=False, methods=["GET"])
def impersonate_end(self, request: Request) -> Response:
"""End Impersonation a user"""
if (
SESSION_KEY_IMPERSONATE_USER not in request.session
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
):
LOGGER.debug("Can't end impersonation", user=request.user)
return Response(status=204)
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
del request.session[SESSION_KEY_IMPERSONATE_USER]
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
return Response(status=204)
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
"""Custom filter_queryset method which ignores guardian, but still supports sorting""" """Custom filter_queryset method which ignores guardian, but still supports sorting"""
for backend in list(self.filter_backends): for backend in list(self.filter_backends):

View File

@ -28,7 +28,7 @@ from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSI
from authentik.lib.utils.urls import redirect_with_qs from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.denied import AccessDeniedResponse from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.utils import delete_none_keys from authentik.policies.utils import delete_none_values
from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
@ -329,7 +329,7 @@ class SourceFlowManager:
) )
], ],
**{ **{
PLAN_CONTEXT_PROMPT: delete_none_keys(self.enroll_info), PLAN_CONTEXT_PROMPT: delete_none_values(self.enroll_info),
PLAN_CONTEXT_USER_PATH: self.source.get_user_path(), PLAN_CONTEXT_USER_PATH: self.source.get_user_path(),
}, },
) )

View File

@ -4,8 +4,8 @@
{% block head %} {% block head %}
<script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script> <script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)"> <meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)">
<link rel="icon" href="{{ tenant.branding_favicon }}"> <link rel="icon" href="{{ tenant.branding_favicon }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}"> <link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
{% include "base/header_js.html" %} {% include "base/header_js.html" %}

View File

@ -1,14 +1,14 @@
"""impersonation tests""" """impersonation tests"""
from json import loads from json import loads
from django.test.testcases import TestCase
from django.urls import reverse from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User 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
class TestImpersonation(TestCase): class TestImpersonation(APITestCase):
"""impersonation tests""" """impersonation tests"""
def setUp(self) -> None: def setUp(self) -> None:
@ -23,10 +23,10 @@ class TestImpersonation(TestCase):
self.other_user.save() self.other_user.save()
self.client.force_login(self.user) self.client.force_login(self.user)
self.client.get( self.client.post(
reverse( reverse(
"authentik_core:impersonate-init", "authentik_api:user-impersonate",
kwargs={"user_id": self.other_user.pk}, kwargs={"pk": self.other_user.pk},
) )
) )
@ -35,7 +35,7 @@ class TestImpersonation(TestCase):
self.assertEqual(response_body["user"]["username"], self.other_user.username) self.assertEqual(response_body["user"]["username"], self.other_user.username)
self.assertEqual(response_body["original"]["username"], self.user.username) self.assertEqual(response_body["original"]["username"], self.user.username)
self.client.get(reverse("authentik_core:impersonate-end")) self.client.get(reverse("authentik_api:user-impersonate-end"))
response = self.client.get(reverse("authentik_api:user-me")) response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode()) response_body = loads(response.content.decode())
@ -46,9 +46,7 @@ class TestImpersonation(TestCase):
"""test impersonation without permissions""" """test impersonation without permissions"""
self.client.force_login(self.other_user) self.client.force_login(self.other_user)
self.client.get( self.client.get(reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}))
reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk})
)
response = self.client.get(reverse("authentik_api:user-me")) response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode()) response_body = loads(response.content.decode())
@ -58,5 +56,5 @@ class TestImpersonation(TestCase):
"""test un-impersonation without impersonating first""" """test un-impersonation without impersonating first"""
self.client.force_login(self.other_user) self.client.force_login(self.other_user)
response = self.client.get(reverse("authentik_core:impersonate-end")) response = self.client.get(reverse("authentik_api:user-impersonate-end"))
self.assertRedirects(response, reverse("authentik_core:if-user")) self.assertEqual(response.status_code, 204)

View File

@ -16,7 +16,7 @@ from authentik.core.api.providers import ProviderViewSet
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
from authentik.core.api.tokens import TokenViewSet from authentik.core.api.tokens import TokenViewSet
from authentik.core.api.users import UserViewSet from authentik.core.api.users import UserViewSet
from authentik.core.views import apps, impersonate from authentik.core.views import apps
from authentik.core.views.debug import AccessDeniedView from authentik.core.views.debug import AccessDeniedView
from authentik.core.views.interface import FlowInterfaceView, InterfaceView from authentik.core.views.interface import FlowInterfaceView, InterfaceView
from authentik.core.views.session import EndSessionView from authentik.core.views.session import EndSessionView
@ -38,17 +38,6 @@ urlpatterns = [
apps.RedirectToAppLaunch.as_view(), apps.RedirectToAppLaunch.as_view(),
name="application-launch", name="application-launch",
), ),
# Impersonation
path(
"-/impersonation/<int:user_id>/",
impersonate.ImpersonateInitView.as_view(),
name="impersonate-init",
),
path(
"-/impersonation/end/",
impersonate.ImpersonateEndView.as_view(),
name="impersonate-end",
),
# Interfaces # Interfaces
path( path(
"if/admin/", "if/admin/",

View File

@ -1,60 +0,0 @@
"""authentik impersonation views"""
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.views import View
from structlog.stdlib import get_logger
from authentik.core.middleware import (
SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
SESSION_KEY_IMPERSONATE_USER,
)
from authentik.core.models import User
from authentik.events.models import Event, EventAction
from authentik.lib.config import CONFIG
LOGGER = get_logger()
class ImpersonateInitView(View):
"""Initiate Impersonation"""
def get(self, request: HttpRequest, user_id: int) -> HttpResponse:
"""Impersonation handler, checks permissions"""
if not CONFIG.y_bool("impersonation"):
LOGGER.debug("User attempted to impersonate", user=request.user)
return HttpResponse("Unauthorized", status=401)
if not request.user.has_perm("impersonate"):
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
return HttpResponse("Unauthorized", status=401)
user_to_be = get_object_or_404(User, pk=user_id)
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
return redirect("authentik_core:if-user")
class ImpersonateEndView(View):
"""End User impersonation"""
def get(self, request: HttpRequest) -> HttpResponse:
"""End Impersonation handler"""
if (
SESSION_KEY_IMPERSONATE_USER not in request.session
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
):
LOGGER.debug("Can't end impersonation", user=request.user)
return redirect("authentik_core:if-user")
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
del request.session[SESSION_KEY_IMPERSONATE_USER]
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
return redirect("authentik_core:root-redirect")

View File

@ -7,7 +7,6 @@ from smtplib import SMTPException
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from uuid import uuid4 from uuid import uuid4
from django.conf import settings
from django.db import models from django.db import models
from django.db.models import Count, ExpressionWrapper, F from django.db.models import Count, ExpressionWrapper, F
from django.db.models.fields import DurationField from django.db.models.fields import DurationField
@ -207,9 +206,7 @@ class Event(SerializerModel, ExpiringModel):
self.user = get_user(user) self.user = get_user(user)
return self return self
def from_http( def from_http(self, request: HttpRequest, user: Optional[User] = None) -> "Event":
self, request: HttpRequest, user: Optional[settings.AUTH_USER_MODEL] = None
) -> "Event":
"""Add data from a Django-HttpRequest, allowing the creation of """Add data from a Django-HttpRequest, allowing the creation of
Events independently from requests. Events independently from requests.
`user` arguments optionally overrides user from requests.""" `user` arguments optionally overrides user from requests."""

View File

@ -23,7 +23,8 @@ class DiagramElement:
style: list[str] = field(default_factory=lambda: ["[", "]"]) style: list[str] = field(default_factory=lambda: ["[", "]"])
def __str__(self) -> str: def __str__(self) -> str:
element = f'{self.identifier}{self.style[0]}"{self.description}"{self.style[1]}' description = self.description.replace('"', "#quot;")
element = f'{self.identifier}{self.style[0]}"{description}"{self.style[1]}'
if self.action is not None: if self.action is not None:
if self.action != "": if self.action != "":
element = f"--{self.action}--> {element}" element = f"--{self.action}--> {element}"

View File

@ -204,12 +204,12 @@ class ChallengeStageView(StageView):
for field, errors in response.errors.items(): for field, errors in response.errors.items():
for error in errors: for error in errors:
full_errors.setdefault(field, []) full_errors.setdefault(field, [])
full_errors[field].append( field_error = {
{ "string": str(error),
"string": str(error), }
"code": error.code, if hasattr(error, "code"):
} field_error["code"] = error.code
) full_errors[field].append(field_error)
challenge_response.initial_data["response_errors"] = full_errors challenge_response.initial_data["response_errors"] = full_errors
if not challenge_response.is_valid(): if not challenge_response.is_valid():
self.logger.error( self.logger.error(

View File

@ -5,18 +5,25 @@ postgresql:
name: authentik name: authentik
user: authentik user: authentik
port: 5432 port: 5432
password: 'env://POSTGRES_PASSWORD' password: "env://POSTGRES_PASSWORD"
use_pgbouncer: false use_pgbouncer: false
listen: listen:
listen_http: 0.0.0.0:9000 listen_http: 0.0.0.0:9000
listen_https: 0.0.0.0:9443 listen_https: 0.0.0.0:9443
listen_metrics: 0.0.0.0:9300 listen_metrics: 0.0.0.0:9300
trusted_proxy_cidrs:
- 127.0.0.0/8
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- fe80::/10
- ::1/128
redis: redis:
host: localhost host: localhost
port: 6379 port: 6379
password: '' password: ""
tls: false tls: false
tls_reqs: "none" tls_reqs: "none"
db: 0 db: 0

View File

@ -140,19 +140,21 @@ class BaseEvaluator:
def expr_event_create(self, action: str, **kwargs): def expr_event_create(self, action: str, **kwargs):
"""Create event with supplied data and try to extract as much relevant data """Create event with supplied data and try to extract as much relevant data
from the context""" from the context"""
context = self._context.copy()
# If the result was a complex variable, we don't want to re-use it # If the result was a complex variable, we don't want to re-use it
self._context.pop("result", None) context.pop("result", None)
self._context.pop("handler", None) context.pop("handler", None)
kwargs["context"] = self._context event_kwargs = context
event_kwargs.update(kwargs)
event = Event.new( event = Event.new(
action, action,
app=self._filename, app=self._filename,
**kwargs, **event_kwargs,
) )
if "request" in self._context and isinstance(self._context["request"], PolicyRequest): if "request" in context and isinstance(context["request"], PolicyRequest):
policy_request: PolicyRequest = self._context["request"] policy_request: PolicyRequest = context["request"]
if policy_request.http_request: if policy_request.http_request:
event.from_http(policy_request) event.from_http(policy_request.http_request)
return return
event.save() event.save()

View File

@ -19,7 +19,15 @@ def fallback_names(app: str, model: str, field: str):
if value not in seen_names: if value not in seen_names:
seen_names.append(value) seen_names.append(value)
continue continue
new_value = value + "_2" separator = "_"
suffix_index = 2
while (
klass.objects.using(db_alias)
.filter(**{field: f"{value}{separator}{suffix_index}"})
.exists()
):
suffix_index += 1
new_value = f"{value}{separator}{suffix_index}"
setattr(obj, field, new_value) setattr(obj, field, new_value)
obj.save() obj.save()

View File

@ -2,28 +2,41 @@
from django.test import TestCase from django.test import TestCase
from authentik.core.tests.utils import create_test_admin_user from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event
from authentik.lib.expression.evaluator import BaseEvaluator from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.generators import generate_id
class TestEvaluator(TestCase): class TestEvaluator(TestCase):
"""Test Evaluator base functions""" """Test Evaluator base functions"""
def test_regex_match(self): def test_expr_regex_match(self):
"""Test expr_regex_match""" """Test expr_regex_match"""
self.assertFalse(BaseEvaluator.expr_regex_match("foo", "bar")) self.assertFalse(BaseEvaluator.expr_regex_match("foo", "bar"))
self.assertTrue(BaseEvaluator.expr_regex_match("foo", "foo")) self.assertTrue(BaseEvaluator.expr_regex_match("foo", "foo"))
def test_regex_replace(self): def test_expr_regex_replace(self):
"""Test expr_regex_replace""" """Test expr_regex_replace"""
self.assertEqual(BaseEvaluator.expr_regex_replace("foo", "o", "a"), "faa") self.assertEqual(BaseEvaluator.expr_regex_replace("foo", "o", "a"), "faa")
def test_user_by(self): def test_expr_user_by(self):
"""Test expr_user_by""" """Test expr_user_by"""
user = create_test_admin_user() user = create_test_admin_user()
self.assertIsNotNone(BaseEvaluator.expr_user_by(username=user.username)) self.assertIsNotNone(BaseEvaluator.expr_user_by(username=user.username))
self.assertIsNone(BaseEvaluator.expr_user_by(username="bar")) self.assertIsNone(BaseEvaluator.expr_user_by(username="bar"))
self.assertIsNone(BaseEvaluator.expr_user_by(foo="bar")) self.assertIsNone(BaseEvaluator.expr_user_by(foo="bar"))
def test_is_group_member(self): def test_expr_is_group_member(self):
"""Test expr_is_group_member""" """Test expr_is_group_member"""
self.assertFalse(BaseEvaluator.expr_is_group_member(create_test_admin_user(), name="test")) self.assertFalse(BaseEvaluator.expr_is_group_member(create_test_admin_user(), name="test"))
def test_expr_event_create(self):
"""Test expr_event_create"""
evaluator = BaseEvaluator(generate_id())
evaluator._context = {
"foo": "bar",
}
evaluator.evaluate("ak_create_event('foo', bar='baz')")
event = Event.objects.filter(action="custom_foo").first()
self.assertIsNotNone(event)
self.assertEqual(event.context, {"bar": "baz", "foo": "bar"})

View File

@ -16,10 +16,12 @@ LOGGER = get_logger()
def _get_client_ip_from_meta(meta: dict[str, Any]) -> str: def _get_client_ip_from_meta(meta: dict[str, Any]) -> str:
"""Attempt to get the client's IP by checking common HTTP Headers. """Attempt to get the client's IP by checking common HTTP Headers.
Returns none if no IP Could be found""" Returns none if no IP Could be found
No additional validation is done here as requests are expected to only arrive here
via the go proxy, which deals with validating these headers for us"""
headers = ( headers = (
"HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED_FOR",
"HTTP_X_REAL_IP",
"REMOTE_ADDR", "REMOTE_ADDR",
) )
for _header in headers: for _header in headers:

View File

@ -42,12 +42,15 @@ from authentik.providers.ldap.controllers.docker import LDAPDockerController
from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController
from authentik.providers.proxy.controllers.docker import ProxyDockerController from authentik.providers.proxy.controllers.docker import ProxyDockerController
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
from authentik.providers.radius.controllers.docker import RadiusDockerController
from authentik.providers.radius.controllers.kubernetes import RadiusKubernetesController
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
LOGGER = get_logger() LOGGER = get_logger()
CACHE_KEY_OUTPOST_DOWN = "goauthentik.io/outposts/teardown/%s" CACHE_KEY_OUTPOST_DOWN = "goauthentik.io/outposts/teardown/%s"
# pylint: disable=too-many-return-statements
def controller_for_outpost(outpost: Outpost) -> Optional[type[BaseController]]: def controller_for_outpost(outpost: Outpost) -> Optional[type[BaseController]]:
"""Get a controller for the outpost, when a service connection is defined""" """Get a controller for the outpost, when a service connection is defined"""
if not outpost.service_connection: if not outpost.service_connection:
@ -63,6 +66,11 @@ def controller_for_outpost(outpost: Outpost) -> Optional[type[BaseController]]:
return LDAPDockerController return LDAPDockerController
if isinstance(service_connection, KubernetesServiceConnection): if isinstance(service_connection, KubernetesServiceConnection):
return LDAPKubernetesController return LDAPKubernetesController
if outpost.type == OutpostType.RADIUS:
if isinstance(service_connection, DockerServiceConnection):
return RadiusDockerController
if isinstance(service_connection, KubernetesServiceConnection):
return RadiusKubernetesController
return None return None

View File

@ -132,9 +132,9 @@ class TestPolicyProcess(TestCase):
) )
binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test")) binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test"))
http_request = self.factory.get(reverse("authentik_core:impersonate-end")) http_request = self.factory.get(reverse("authentik_api:user-impersonate-end"))
http_request.user = self.user http_request.user = self.user
http_request.resolver_match = resolve(reverse("authentik_core:impersonate-end")) http_request.resolver_match = resolve(reverse("authentik_api:user-impersonate-end"))
request = PolicyRequest(self.user) request = PolicyRequest(self.user)
request.set_http_request(http_request) request.set_http_request(http_request)

View File

@ -2,7 +2,7 @@
from typing import Any from typing import Any
def delete_none_keys(dict_: dict[Any, Any]) -> dict[Any, Any]: def delete_none_values(dict_: dict[Any, Any]) -> dict[Any, Any]:
"""Remove any keys from `dict_` that are None.""" """Remove any keys from `dict_` that are None."""
new_dict = {} new_dict = {}
for key, value in dict_.items(): for key, value in dict_.items():

View File

@ -1,4 +1,8 @@
"""LDAPProvider API Views""" """LDAPProvider API Views"""
from django.db.models import QuerySet
from django.db.models.query import Q
from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet
from rest_framework.fields import CharField, ListField, SerializerMethodField from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
@ -29,24 +33,41 @@ class LDAPProviderSerializer(ProviderSerializer):
extra_kwargs = ProviderSerializer.Meta.extra_kwargs extra_kwargs = ProviderSerializer.Meta.extra_kwargs
class LDAPProviderFilter(FilterSet):
"""LDAP Provider filters"""
application__isnull = BooleanFilter(method="filter_application__isnull")
def filter_application__isnull(self, queryset: QuerySet, name, value):
"""Only return providers that are neither assigned to application,
both as provider or application provider"""
return queryset.filter(
Q(backchannel_application__isnull=value) | Q(application__isnull=value)
)
class Meta:
model = LDAPProvider
fields = {
"application": ["isnull"],
"name": ["iexact"],
"authorization_flow__slug": ["iexact"],
"base_dn": ["iexact"],
"search_group__group_uuid": ["iexact"],
"search_group__name": ["iexact"],
"certificate__kp_uuid": ["iexact"],
"certificate__name": ["iexact"],
"tls_server_name": ["iexact"],
"uid_start_number": ["iexact"],
"gid_start_number": ["iexact"],
}
class LDAPProviderViewSet(UsedByMixin, ModelViewSet): class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
"""LDAPProvider Viewset""" """LDAPProvider Viewset"""
queryset = LDAPProvider.objects.all() queryset = LDAPProvider.objects.all()
serializer_class = LDAPProviderSerializer serializer_class = LDAPProviderSerializer
filterset_fields = { filterset_class = LDAPProviderFilter
"application": ["isnull"],
"name": ["iexact"],
"authorization_flow__slug": ["iexact"],
"base_dn": ["iexact"],
"search_group__group_uuid": ["iexact"],
"search_group__name": ["iexact"],
"certificate__kp_uuid": ["iexact"],
"certificate__name": ["iexact"],
"tls_server_name": ["iexact"],
"uid_start_number": ["iexact"],
"gid_start_number": ["iexact"],
}
search_fields = ["name"] search_fields = ["name"]
ordering = ["name"] ordering = ["name"]

View File

@ -1,5 +1,5 @@
"""RadiusProvider API Views""" """RadiusProvider API Views"""
from rest_framework.fields import CharField from rest_framework.fields import CharField, ListField
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
@ -11,6 +11,8 @@ from authentik.providers.radius.models import RadiusProvider
class RadiusProviderSerializer(ProviderSerializer): class RadiusProviderSerializer(ProviderSerializer):
"""RadiusProvider Serializer""" """RadiusProvider Serializer"""
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
class Meta: class Meta:
model = RadiusProvider model = RadiusProvider
fields = ProviderSerializer.Meta.fields + [ fields = ProviderSerializer.Meta.fields + [
@ -18,6 +20,7 @@ class RadiusProviderSerializer(ProviderSerializer):
# Shared secret is not a write-only field, as # Shared secret is not a write-only field, as
# an admin might have to view it # an admin might have to view it
"shared_secret", "shared_secret",
"outpost_set",
] ]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs extra_kwargs = ProviderSerializer.Meta.extra_kwargs

View File

@ -24,8 +24,8 @@ class SCIMProviderSerializer(ProviderSerializer):
"property_mappings", "property_mappings",
"property_mappings_group", "property_mappings_group",
"component", "component",
"assigned_application_slug", "assigned_backchannel_application_slug",
"assigned_application_name", "assigned_backchannel_application_name",
"verbose_name", "verbose_name",
"verbose_name_plural", "verbose_name_plural",
"meta_model_name", "meta_model_name",

View File

@ -8,7 +8,7 @@ from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.models import Group from authentik.core.models import Group
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.errors import exception_to_string
from authentik.policies.utils import delete_none_keys from authentik.policies.utils import delete_none_values
from authentik.providers.scim.clients.base import SCIMClient from authentik.providers.scim.clients.base import SCIMClient
from authentik.providers.scim.clients.exceptions import ( from authentik.providers.scim.clients.exceptions import (
ResourceMissing, ResourceMissing,
@ -74,7 +74,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
if not raw_scim_group: if not raw_scim_group:
raise StopSync(ValueError("No group mappings configured"), obj) raise StopSync(ValueError("No group mappings configured"), obj)
try: try:
scim_group = SCIMGroupSchema.parse_obj(delete_none_keys(raw_scim_group)) scim_group = SCIMGroupSchema.parse_obj(delete_none_values(raw_scim_group))
except ValidationError as exc: except ValidationError as exc:
raise StopSync(exc, obj) from exc raise StopSync(exc, obj) from exc
if not scim_group.externalId: if not scim_group.externalId:

View File

@ -6,7 +6,7 @@ from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.models import User from authentik.core.models import User
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.errors import exception_to_string
from authentik.policies.utils import delete_none_keys from authentik.policies.utils import delete_none_values
from authentik.providers.scim.clients.base import SCIMClient from authentik.providers.scim.clients.base import SCIMClient
from authentik.providers.scim.clients.exceptions import ResourceMissing, StopSync from authentik.providers.scim.clients.exceptions import ResourceMissing, StopSync
from authentik.providers.scim.clients.schema import User as SCIMUserSchema from authentik.providers.scim.clients.schema import User as SCIMUserSchema
@ -64,7 +64,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
if not raw_scim_user: if not raw_scim_user:
raise StopSync(ValueError("No user mappings configured"), obj) raise StopSync(ValueError("No user mappings configured"), obj)
try: try:
scim_user = SCIMUserSchema.parse_obj(delete_none_keys(raw_scim_user)) scim_user = SCIMUserSchema.parse_obj(delete_none_values(raw_scim_user))
except ValidationError as exc: except ValidationError as exc:
raise StopSync(exc, obj) from exc raise StopSync(exc, obj) from exc
if not scim_user.externalId: if not scim_user.externalId:

View File

@ -42,7 +42,9 @@ def scim_sync_all():
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=MonitoredTask)
def scim_sync(self: MonitoredTask, provider_pk: int) -> None: def scim_sync(self: MonitoredTask, provider_pk: int) -> None:
"""Run SCIM full sync for provider""" """Run SCIM full sync for provider"""
provider: SCIMProvider = SCIMProvider.objects.filter(pk=provider_pk).first() provider: SCIMProvider = SCIMProvider.objects.filter(
pk=provider_pk, backchannel_application__isnull=False
).first()
if not provider: if not provider:
return return
self.set_uid(slugify(provider.name)) self.set_uid(slugify(provider.name))

View File

@ -36,6 +36,7 @@ class SCIMMembershipTests(TestCase):
slug=generate_id(), slug=generate_id(),
) )
self.app.backchannel_providers.add(self.provider) self.app.backchannel_providers.add(self.provider)
self.provider.save()
self.provider.property_mappings.set( self.provider.property_mappings.set(
[SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")] [SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")]
) )
@ -91,7 +92,6 @@ class SCIMMembershipTests(TestCase):
"active": True, "active": True,
"externalId": user.uid, "externalId": user.uid,
"name": {"familyName": "", "formatted": "", "givenName": ""}, "name": {"familyName": "", "formatted": "", "givenName": ""},
"photos": [],
"displayName": "", "displayName": "",
"userName": user.username, "userName": user.username,
}, },
@ -177,7 +177,6 @@ class SCIMMembershipTests(TestCase):
"emails": [], "emails": [],
"externalId": user.uid, "externalId": user.uid,
"name": {"familyName": "", "formatted": "", "givenName": ""}, "name": {"familyName": "", "formatted": "", "givenName": ""},
"photos": [],
"userName": user.username, "userName": user.username,
}, },
) )

View File

@ -81,7 +81,6 @@ class SCIMUserTests(TestCase):
"givenName": uid, "givenName": uid,
}, },
"displayName": uid, "displayName": uid,
"photos": [],
"userName": uid, "userName": uid,
}, },
) )
@ -137,7 +136,6 @@ class SCIMUserTests(TestCase):
"formatted": uid, "formatted": uid,
"givenName": uid, "givenName": uid,
}, },
"photos": [],
"userName": uid, "userName": uid,
}, },
) )
@ -190,7 +188,6 @@ class SCIMUserTests(TestCase):
"givenName": uid, "givenName": uid,
}, },
"displayName": uid, "displayName": uid,
"photos": [],
"userName": uid, "userName": uid,
}, },
) )
@ -258,7 +255,6 @@ class SCIMUserTests(TestCase):
"givenName": uid, "givenName": uid,
}, },
"displayName": uid, "displayName": uid,
"photos": [],
"userName": uid, "userName": uid,
}, },
) )

View File

@ -55,7 +55,7 @@ class LDAPBackend(InbuiltBackend):
"""Attempt authentication by binding to the LDAP server as `user`. This """Attempt authentication by binding to the LDAP server as `user`. This
method should be avoided as its slow to do the bind.""" method should be avoided as its slow to do the bind."""
# Try to bind as new user # Try to bind as new user
LOGGER.debug("Attempting Binding as user", user=user) LOGGER.debug("Attempting to bind as user", user=user)
try: try:
temp_connection = source.connection( temp_connection = source.connection(
connection_kwargs={ connection_kwargs={
@ -65,8 +65,8 @@ class LDAPBackend(InbuiltBackend):
) )
temp_connection.bind() temp_connection.bind()
return user return user
except LDAPInvalidCredentialsResult as exception: except LDAPInvalidCredentialsResult as exc:
LOGGER.debug("LDAPInvalidCredentialsResult", user=user, error=exception) LOGGER.debug("invalid LDAP credentials", user=user, exc=exc)
except LDAPException as exception: except LDAPException as exc:
LOGGER.warning(exception) LOGGER.warning("failed to bind to LDAP", exc=exc)
return None return None

View File

@ -6,6 +6,7 @@ from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from ldap3.core.exceptions import LDAPOperationResult from ldap3.core.exceptions import LDAPOperationResult
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
from authentik.core.models import User from authentik.core.models import User
from authentik.core.signals import password_changed from authentik.core.signals import password_changed
@ -20,6 +21,8 @@ from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
from authentik.sources.ldap.tasks import ldap_sync from authentik.sources.ldap.tasks import ldap_sync
from authentik.stages.prompt.signals import password_validate from authentik.stages.prompt.signals import password_validate
LOGGER = get_logger()
@receiver(post_save, sender=LDAPSource) @receiver(post_save, sender=LDAPSource)
def sync_ldap_source_on_save(sender, instance: LDAPSource, **_): def sync_ldap_source_on_save(sender, instance: LDAPSource, **_):
@ -63,13 +66,17 @@ def ldap_sync_password(sender, user: User, password: str, **_):
if not sources.exists(): if not sources.exists():
return return
source = sources.first() source = sources.first()
changer = LDAPPasswordChanger(source)
try: try:
changer = LDAPPasswordChanger(source)
changer.change_password(user, password) changer.change_password(user, password)
except LDAPOperationResult as exc: except LDAPOperationResult as exc:
LOGGER.warning("failed to set LDAP password", exc=exc)
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
message=f"Result: {exc.result}, Description {exc.description}", message=(
"Failed to change password in LDAP source due to remote error: "
f"{exc.result}, {exc.message}, {exc.description}"
),
source=source, source=source,
).set_user(user).save() ).set_user(user).save()
raise ValidationError("Failed to set password") from exc raise ValidationError("Failed to set password") from exc

View File

@ -135,9 +135,9 @@ class BaseLDAPSynchronizer:
if key == "attributes": if key == "attributes":
continue continue
setattr(instance, key, value) setattr(instance, key, value)
final_atttributes = {} final_attributes = {}
MERGE_LIST_UNIQUE.merge(final_atttributes, instance.attributes) MERGE_LIST_UNIQUE.merge(final_attributes, instance.attributes)
MERGE_LIST_UNIQUE.merge(final_atttributes, data.get("attributes", {})) MERGE_LIST_UNIQUE.merge(final_attributes, data.get("attributes", {}))
instance.attributes = final_atttributes instance.attributes = final_attributes
instance.save() instance.save()
return (instance, False) return (instance, False)

View File

@ -21,7 +21,7 @@ from authentik.core.models import (
from authentik.core.sources.flow_manager import SourceFlowManager from authentik.core.sources.flow_manager import SourceFlowManager
from authentik.lib.expression.evaluator import BaseEvaluator from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.utils import delete_none_keys from authentik.policies.utils import delete_none_values
from authentik.sources.saml.exceptions import ( from authentik.sources.saml.exceptions import (
InvalidSignature, InvalidSignature,
MismatchedRequestID, MismatchedRequestID,
@ -160,7 +160,7 @@ class ResponseProcessor:
self._source, self._source,
self._http_request, self._http_request,
name_id, name_id,
delete_none_keys(self.get_attributes()), delete_none_values(self.get_attributes()),
) )
def _get_name_id(self) -> "Element": def _get_name_id(self) -> "Element":
@ -237,7 +237,7 @@ class ResponseProcessor:
self._source, self._source,
self._http_request, self._http_request,
name_id.text, name_id.text,
delete_none_keys(self.get_attributes()), delete_none_values(self.get_attributes()),
) )

View File

@ -133,6 +133,12 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first() device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
if not device: if not device:
raise ValidationError("Invalid device") raise ValidationError("Invalid device")
# We can only check the device's user if the user we're given isn't anonymous
# as this validation is also used for password-less login where webauthn is the very first
# step done by a user. Only if this validation happens at a later stage we can check
# that the device belongs to the user
if not user.is_anonymous and device.user != user:
raise ValidationError("Invalid device")
stage: AuthenticatorValidateStage = stage_view.executor.current_stage stage: AuthenticatorValidateStage = stage_view.executor.current_stage

View File

@ -36,9 +36,9 @@ from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_ME
COOKIE_NAME_MFA = "authentik_mfa" COOKIE_NAME_MFA = "authentik_mfa"
SESSION_KEY_STAGES = "authentik/stages/authenticator_validate/stages" PLAN_CONTEXT_STAGES = "goauthentik.io/stages/authenticator_validate/stages"
SESSION_KEY_SELECTED_STAGE = "authentik/stages/authenticator_validate/selected_stage" PLAN_CONTEXT_SELECTED_STAGE = "goauthentik.io/stages/authenticator_validate/selected_stage"
SESSION_KEY_DEVICE_CHALLENGES = "authentik/stages/authenticator_validate/device_challenges" PLAN_CONTEXT_DEVICE_CHALLENGES = "goauthentik.io/stages/authenticator_validate/device_challenges"
class SelectableStageSerializer(PassiveSerializer): class SelectableStageSerializer(PassiveSerializer):
@ -72,8 +72,8 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
component = CharField(default="ak-stage-authenticator-validate") component = CharField(default="ak-stage-authenticator-validate")
def _challenge_allowed(self, classes: list): def _challenge_allowed(self, classes: list):
device_challenges: list[dict] = self.stage.request.session.get( device_challenges: list[dict] = self.stage.executor.plan.context.get(
SESSION_KEY_DEVICE_CHALLENGES, [] PLAN_CONTEXT_DEVICE_CHALLENGES, []
) )
if not any(x["device_class"] in classes for x in device_challenges): if not any(x["device_class"] in classes for x in device_challenges):
raise ValidationError("No compatible device class allowed") raise ValidationError("No compatible device class allowed")
@ -103,7 +103,9 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
"""Check which challenge the user has selected. Actual logic only used for SMS stage.""" """Check which challenge the user has selected. Actual logic only used for SMS stage."""
# First check if the challenge is valid # First check if the challenge is valid
allowed = False allowed = False
for device_challenge in self.stage.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, []): for device_challenge in self.stage.executor.plan.context.get(
PLAN_CONTEXT_DEVICE_CHALLENGES, []
):
if device_challenge.get("device_class", "") == challenge.get( if device_challenge.get("device_class", "") == challenge.get(
"device_class", "" "device_class", ""
) and device_challenge.get("device_uid", "") == challenge.get("device_uid", ""): ) and device_challenge.get("device_uid", "") == challenge.get("device_uid", ""):
@ -121,11 +123,11 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
def validate_selected_stage(self, stage_pk: str) -> str: def validate_selected_stage(self, stage_pk: str) -> str:
"""Check that the selected stage is valid""" """Check that the selected stage is valid"""
stages = self.stage.request.session.get(SESSION_KEY_STAGES, []) stages = self.stage.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
if not any(str(stage.pk) == stage_pk for stage in stages): if not any(str(stage.pk) == stage_pk for stage in stages):
raise ValidationError("Selected stage is invalid") raise ValidationError("Selected stage is invalid")
self.stage.logger.debug("Setting selected stage to ", stage=stage_pk) self.stage.logger.debug("Setting selected stage to ", stage=stage_pk)
self.stage.request.session[SESSION_KEY_SELECTED_STAGE] = stage_pk self.stage.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = stage_pk
return stage_pk return stage_pk
def validate(self, attrs: dict): def validate(self, attrs: dict):
@ -230,7 +232,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
else: else:
self.logger.debug("No pending user, continuing") self.logger.debug("No pending user, continuing")
return self.executor.stage_ok() return self.executor.stage_ok()
self.request.session[SESSION_KEY_DEVICE_CHALLENGES] = challenges self.executor.plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = challenges
# No allowed devices # No allowed devices
if len(challenges) < 1: if len(challenges) < 1:
@ -263,23 +265,23 @@ class AuthenticatorValidateStageView(ChallengeStageView):
if stage.configuration_stages.count() == 1: if stage.configuration_stages.count() == 1:
next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk) next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk)
self.logger.debug("Single stage configured, auto-selecting", stage=next_stage) self.logger.debug("Single stage configured, auto-selecting", stage=next_stage)
self.request.session[SESSION_KEY_SELECTED_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 # Because that normal execution only happens on post, we directly inject it here and
# return it # return it
self.executor.plan.insert_stage(next_stage) self.executor.plan.insert_stage(next_stage)
return self.executor.stage_ok() return self.executor.stage_ok()
stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses() stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses()
self.request.session[SESSION_KEY_STAGES] = stages self.executor.plan.context[PLAN_CONTEXT_STAGES] = stages
return super().get(self.request, *args, **kwargs) return super().get(self.request, *args, **kwargs)
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
res = super().post(request, *args, **kwargs) res = super().post(request, *args, **kwargs)
if ( if (
SESSION_KEY_SELECTED_STAGE in self.request.session PLAN_CONTEXT_SELECTED_STAGE in self.executor.plan.context
and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
): ):
self.logger.debug("Got selected stage in session, running that") self.logger.debug("Got selected stage in context, running that")
stage_pk = self.request.session.get(SESSION_KEY_SELECTED_STAGE) stage_pk = self.executor.plan.context(PLAN_CONTEXT_SELECTED_STAGE)
# Because the foreign key to stage.configuration_stage points to # Because the foreign key to stage.configuration_stage points to
# a base stage class, we need to do another lookup # a base stage class, we need to do another lookup
stage = Stage.objects.get_subclass(pk=stage_pk) stage = Stage.objects.get_subclass(pk=stage_pk)
@ -290,8 +292,8 @@ class AuthenticatorValidateStageView(ChallengeStageView):
return res return res
def get_challenge(self) -> AuthenticatorValidationChallenge: def get_challenge(self) -> AuthenticatorValidationChallenge:
challenges = self.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, []) challenges = self.executor.plan.context.get(PLAN_CONTEXT_DEVICE_CHALLENGES, [])
stages = self.request.session.get(SESSION_KEY_STAGES, []) stages = self.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
stage_challenges = [] stage_challenges = []
for stage in stages: for stage in stages:
serializer = SelectableStageSerializer( serializer = SelectableStageSerializer(
@ -306,6 +308,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
stage_challenges.append(serializer.data) stage_challenges.append(serializer.data)
return AuthenticatorValidationChallenge( return AuthenticatorValidationChallenge(
data={ data={
"component": "ak-stage-authenticator-validate",
"type": ChallengeTypes.NATIVE.value, "type": ChallengeTypes.NATIVE.value,
"device_challenges": challenges, "device_challenges": challenges,
"configuration_stages": stage_challenges, "configuration_stages": stage_challenges,
@ -385,8 +388,3 @@ class AuthenticatorValidateStageView(ChallengeStageView):
"device": webauthn_device, "device": webauthn_device,
} }
return self.set_valid_mfa_cookie(response.device) return self.set_valid_mfa_cookie(response.device)
def cleanup(self):
self.request.session.pop(SESSION_KEY_STAGES, None)
self.request.session.pop(SESSION_KEY_SELECTED_STAGE, None)
self.request.session.pop(SESSION_KEY_DEVICE_CHALLENGES, None)

View File

@ -1,26 +1,19 @@
"""Test validator stage""" """Test validator stage"""
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from django.contrib.sessions.middleware import SessionMiddleware
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.urls.base import reverse from django.urls.base import reverse
from rest_framework.exceptions import ValidationError
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
from authentik.flows.planner import FlowPlan from authentik.flows.planner import FlowPlan
from authentik.flows.stage import StageView
from authentik.flows.tests import FlowTestCase from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN, FlowExecutorView from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id, generate_key from authentik.lib.generators import generate_id, generate_key
from authentik.lib.tests.utils import dummy_get_response
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.authenticator_validate.stage import ( from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES
SESSION_KEY_DEVICE_CHALLENGES,
AuthenticatorValidationChallengeResponse,
)
from authentik.stages.identification.models import IdentificationStage, UserFields from authentik.stages.identification.models import IdentificationStage, UserFields
@ -86,12 +79,17 @@ class AuthenticatorValidateStageTests(FlowTestCase):
def test_validate_selected_challenge(self): def test_validate_selected_challenge(self):
"""Test validate_selected_challenge""" """Test validate_selected_challenge"""
# Prepare request with session flow = create_test_flow()
request = self.request_factory.get("/") stage = AuthenticatorValidateStage.objects.create(
name=generate_id(),
not_configured_action=NotConfiguredAction.CONFIGURE,
device_classes=[DeviceClasses.STATIC, DeviceClasses.TOTP],
)
middleware = SessionMiddleware(dummy_get_response) session = self.client.session
middleware.process_request(request) plan = FlowPlan(flow_pk=flow.pk.hex)
request.session[SESSION_KEY_DEVICE_CHALLENGES] = [ plan.append_stage(stage)
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
{ {
"device_class": "static", "device_class": "static",
"device_uid": "1", "device_uid": "1",
@ -101,23 +99,43 @@ class AuthenticatorValidateStageTests(FlowTestCase):
"device_uid": "2", "device_uid": "2",
}, },
] ]
request.session.save() session[SESSION_KEY_PLAN] = plan
session.save()
res = AuthenticatorValidationChallengeResponse() response = self.client.post(
res.stage = StageView(FlowExecutorView()) reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
res.stage.request = request data={
with self.assertRaises(ValidationError): "selected_challenge": {
res.validate_selected_challenge(
{
"device_class": "baz", "device_class": "baz",
"device_uid": "quox", "device_uid": "quox",
"challenge": {},
} }
) },
res.validate_selected_challenge( )
{ self.assertStageResponse(
"device_class": "static", response,
"device_uid": "1", flow,
} response_errors={
"selected_challenge": [{"string": "invalid challenge selected", "code": "invalid"}]
},
component="ak-stage-authenticator-validate",
)
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
data={
"selected_challenge": {
"device_class": "static",
"device_uid": "1",
"challenge": {},
},
},
)
self.assertStageResponse(
response,
flow,
response_errors={"non_field_errors": [{"string": "Empty response", "code": "invalid"}]},
component="ak-stage-authenticator-validate",
) )
@patch( @patch(

View File

@ -22,7 +22,7 @@ from authentik.stages.authenticator_validate.challenge import (
) )
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.authenticator_validate.stage import ( from authentik.stages.authenticator_validate.stage import (
SESSION_KEY_DEVICE_CHALLENGES, PLAN_CONTEXT_DEVICE_CHALLENGES,
AuthenticatorValidateStageView, AuthenticatorValidateStageView,
) )
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
@ -211,14 +211,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
plan.append_stage(stage) plan.append_stage(stage)
plan.append_stage(UserLoginStage(name=generate_id())) plan.append_stage(UserLoginStage(name=generate_id()))
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session[SESSION_KEY_PLAN] = plan plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
session[SESSION_KEY_DEVICE_CHALLENGES] = [
{ {
"device_class": device.__class__.__name__.lower().replace("device", ""), "device_class": device.__class__.__name__.lower().replace("device", ""),
"device_uid": device.pk, "device_uid": device.pk,
"challenge": {}, "challenge": {},
} }
] ]
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
) )
@ -283,14 +283,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
plan = FlowPlan(flow_pk=flow.pk.hex) plan = FlowPlan(flow_pk=flow.pk.hex)
plan.append_stage(stage) plan.append_stage(stage)
plan.append_stage(UserLoginStage(name=generate_id())) plan.append_stage(UserLoginStage(name=generate_id()))
session[SESSION_KEY_PLAN] = plan plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
session[SESSION_KEY_DEVICE_CHALLENGES] = [
{ {
"device_class": device.__class__.__name__.lower().replace("device", ""), "device_class": device.__class__.__name__.lower().replace("device", ""),
"device_uid": device.pk, "device_uid": device.pk,
"challenge": {}, "challenge": {},
} }
] ]
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
) )

View File

@ -8,7 +8,7 @@ from authentik.flows.models import Stage
class DenyStage(Stage): class DenyStage(Stage):
"""Cancells the current flow.""" """Cancels the current flow."""
@property @property
def serializer(self) -> type[BaseSerializer]: def serializer(self) -> type[BaseSerializer]:

View File

@ -5,10 +5,10 @@ from authentik.flows.stage import StageView
class DenyStageView(StageView): class DenyStageView(StageView):
"""Cancells the current flow""" """Cancels the current flow"""
def get(self, request: HttpRequest) -> HttpResponse: def get(self, request: HttpRequest) -> HttpResponse:
"""Cancells the current flow""" """Cancels the current flow"""
return self.executor.stage_invalid() return self.executor.stage_invalid()
def post(self, request: HttpRequest) -> HttpResponse: def post(self, request: HttpRequest) -> HttpResponse:

View File

@ -12,7 +12,7 @@ from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.models import FlowToken from authentik.flows.models import FlowDesignation, FlowToken
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN from authentik.flows.views.executor import QS_KEY_TOKEN
@ -82,6 +82,11 @@ class EmailStageView(ChallengeStageView):
"""Helper function that sends the actual email. Implies that you've """Helper function that sends the actual email. Implies that you've
already checked that there is a pending user.""" already checked that there is a pending user."""
pending_user = self.get_pending_user() pending_user = self.get_pending_user()
if not pending_user.pk and self.executor.flow.designation == FlowDesignation.RECOVERY:
# Pending user does not have a primary key, and we're in a recovery flow,
# which means the user entered an invalid identifier, so we pretend to send the
# email, to not disclose if the user exists
return
email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None) email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
if not email: if not email:
email = pending_user.email email = pending_user.email

View File

@ -5,18 +5,20 @@ from unittest.mock import MagicMock, PropertyMock, patch
from django.core import mail from django.core import mail
from django.core.mail.backends.locmem import EmailBackend from django.core.mail.backends.locmem import EmailBackend
from django.urls import reverse from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding from authentik.flows.models import FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
class TestEmailStageSending(APITestCase): class TestEmailStageSending(FlowTestCase):
"""Email tests""" """Email tests"""
def setUp(self): def setUp(self):
@ -44,6 +46,13 @@ class TestEmailStageSending(APITestCase):
): ):
response = self.client.post(url) response = self.client.post(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
self.flow,
response_errors={
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
},
)
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "authentik") self.assertEqual(mail.outbox[0].subject, "authentik")
events = Event.objects.filter(action=EventAction.EMAIL_SENT) events = Event.objects.filter(action=EventAction.EMAIL_SENT)
@ -54,6 +63,32 @@ class TestEmailStageSending(APITestCase):
self.assertEqual(event.context["to_email"], [self.user.email]) self.assertEqual(event.context["to_email"], [self.user.email])
self.assertEqual(event.context["from_email"], "system@authentik.local") self.assertEqual(event.context["from_email"], "system@authentik.local")
def test_pending_fake_user(self):
"""Test with pending (fake) user"""
self.flow.designation = FlowDesignation.RECOVERY
self.flow.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = User(username=generate_id())
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
with patch(
"authentik.stages.email.models.EmailStage.backend_class",
PropertyMock(return_value=EmailBackend),
):
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
self.flow,
response_errors={
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
},
)
self.assertEqual(len(mail.outbox), 0)
def test_send_error(self): def test_send_error(self):
"""Test error during sending (sending will be retried)""" """Test error during sending (sending will be retried)"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])

View File

@ -118,8 +118,12 @@ class IdentificationChallengeResponse(ChallengeResponse):
username=uid_field, username=uid_field,
email=uid_field, email=uid_field,
) )
self.pre_user = self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
if not current_stage.show_matched_user: if not current_stage.show_matched_user:
self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = uid_field self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = uid_field
if self.stage.executor.flow.designation == FlowDesignation.RECOVERY:
# When used in a recovery flow, always continue to not disclose if a user exists
return attrs
raise ValidationError("Failed to authenticate.") raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user self.pre_user = pre_user
if not current_stage.password_stage: if not current_stage.password_stage:

View File

@ -188,7 +188,7 @@ class TestIdentificationStage(FlowTestCase):
], ],
) )
def test_recovery_flow(self): def test_link_recovery_flow(self):
"""Test that recovery flow is linked correctly""" """Test that recovery flow is linked correctly"""
flow = create_test_flow() flow = create_test_flow()
self.stage.recovery_flow = flow self.stage.recovery_flow = flow
@ -226,6 +226,38 @@ class TestIdentificationStage(FlowTestCase):
], ],
) )
def test_recovery_flow_invalid_user(self):
"""Test that an invalid user can proceed in a recovery flow"""
self.flow.designation = FlowDesignation.RECOVERY
self.flow.save()
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
)
self.assertStageResponse(
response,
self.flow,
component="ak-stage-identification",
user_fields=["email"],
password_fields=False,
show_source_labels=False,
primary_action="Continue",
sources=[
{
"challenge": {
"component": "xak-flow-redirect",
"to": "/source/oauth/login/test/",
"type": ChallengeTypes.REDIRECT.value,
},
"icon_url": "/static/authentik/sources/default.svg",
"name": "test",
}
],
)
form_data = {"uid_field": generate_id()}
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
response = self.client.post(url, form_data)
self.assertEqual(response.status_code, 200)
def test_api_validate(self): def test_api_validate(self):
"""Test API validation""" """Test API validation"""
self.assertTrue( self.assertTrue(

View File

@ -6,6 +6,7 @@ from django.db import transaction
from django.db.utils import IntegrityError, InternalError from django.db.utils import IntegrityError, InternalError
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from rest_framework.exceptions import ValidationError
from authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER from authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER
from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection
@ -148,7 +149,11 @@ class UserWriteStageView(StageView):
and SESSION_KEY_IMPERSONATE_USER not in self.request.session and SESSION_KEY_IMPERSONATE_USER not in self.request.session
): ):
should_update_session = True should_update_session = True
self.update_user(user) try:
self.update_user(user)
except ValidationError as exc:
self.logger.warning("failed to update user", exc=exc)
return self.executor.stage_invalid(_("Failed to update user. Please try again later."))
# Extra check to prevent flows from saving a user with a blank username # Extra check to prevent flows from saving a user with a blank username
if user.username == "": if user.username == "":
self.logger.warning("Aborting write to empty username", user=user) self.logger.warning("Aborting write to empty username", user=user)
@ -162,7 +167,7 @@ class UserWriteStageView(StageView):
user.ak_groups.add(*self.executor.plan.context[PLAN_CONTEXT_GROUPS]) user.ak_groups.add(*self.executor.plan.context[PLAN_CONTEXT_GROUPS])
except (IntegrityError, ValueError, TypeError, InternalError) as exc: except (IntegrityError, ValueError, TypeError, InternalError) as exc:
self.logger.warning("Failed to save user", exc=exc) self.logger.warning("Failed to save user", exc=exc)
return self.executor.stage_invalid(_("Failed to save user")) return self.executor.stage_invalid(_("Failed to update user. Please try again later."))
user_write.send(sender=self, request=request, user=user, data=data, created=user_created) user_write.send(sender=self, request=request, user=user, data=data, created=user_created)
# Check if the password has been updated, and update the session auth hash # Check if the password has been updated, and update the session auth hash
if should_update_session: if should_update_session:

View File

@ -3888,8 +3888,7 @@
}, },
"required": [ "required": [
"username", "username",
"name", "name"
"groups"
], ],
"title": "User" "title": "User"
}, },
@ -4080,8 +4079,7 @@
}, },
"required": [ "required": [
"username", "username",
"name", "name"
"groups"
], ],
"title": "User" "title": "User"
}, },
@ -4276,8 +4274,7 @@
}, },
"required": [ "required": [
"username", "username",
"name", "name"
"groups"
], ],
"title": "User" "title": "User"
}, },
@ -6419,8 +6416,7 @@
}, },
"required": [ "required": [
"username", "username",
"name", "name"
"groups"
], ],
"title": "User" "title": "User"
}, },
@ -7155,8 +7151,7 @@
}, },
"required": [ "required": [
"username", "username",
"name", "name"
"groups"
], ],
"title": "User" "title": "User"
}, },

View File

@ -21,7 +21,7 @@ entries:
# photos supports URLs to images, however authentik might return data URIs # photos supports URLs to images, however authentik might return data URIs
avatar = request.user.avatar avatar = request.user.avatar
photos = [] photos = None
if "://" in avatar: if "://" in avatar:
photos = [{"value": avatar, "type": "photo"}] photos = [{"value": avatar, "type": "photo"}]
@ -31,11 +31,11 @@ entries:
emails = [] emails = []
if request.user.email != "": if request.user.email != "":
emails.append({ emails = [{
"value": request.user.email, "value": request.user.email,
"type": "other", "type": "other",
"primary": True, "primary": True,
}) }]
return { return {
"userName": request.user.username, "userName": request.user.username,
"name": { "name": {

View File

@ -32,7 +32,7 @@ services:
volumes: volumes:
- redis:/data - redis:/data
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.0} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.6}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -50,7 +50,7 @@ services:
- "${COMPOSE_PORT_HTTP:-9000}:9000" - "${COMPOSE_PORT_HTTP:-9000}:9000"
- "${COMPOSE_PORT_HTTPS:-9443}:9443" - "${COMPOSE_PORT_HTTPS:-9443}:9443"
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.0} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.6}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
environment: environment:

4
go.mod
View File

@ -23,10 +23,10 @@ require (
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba
github.com/pires/go-proxyproto v0.7.0 github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.15.1 github.com/prometheus/client_golang v1.15.1
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.2
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.2 github.com/stretchr/testify v1.8.2
goauthentik.io/api/v3 v3.2023041.12 goauthentik.io/api/v3 v3.2023050.2
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.8.0 golang.org/x/oauth2 v0.8.0
golang.org/x/sync v0.2.0 golang.org/x/sync v0.2.0

8
go.sum
View File

@ -200,8 +200,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
@ -241,8 +241,8 @@ go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvx
go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
goauthentik.io/api/v3 v3.2023041.12 h1:lk8eCWYW/P8U4r10RgtIq2NyaAqZ3KKrKc7eierV6aY= goauthentik.io/api/v3 v3.2023050.2 h1:EnwEaPM2qSFwfow0G/pTk9GHXmux0ldN77b+/gMeGTM=
goauthentik.io/api/v3 v3.2023041.12/go.mod h1:nYECml4jGbp/541hj8GcylKQG1gVBsKppHy4+7G8u4U= goauthentik.io/api/v3 v3.2023050.2/go.mod h1:nYECml4jGbp/541hj8GcylKQG1gVBsKppHy4+7G8u4U=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=

View File

@ -38,13 +38,14 @@ type RedisConfig struct {
} }
type ListenConfig struct { type ListenConfig struct {
HTTP string `yaml:"listen_http" env:"AUTHENTIK_LISTEN__HTTP"` HTTP string `yaml:"listen_http" env:"AUTHENTIK_LISTEN__HTTP"`
HTTPS string `yaml:"listen_https" env:"AUTHENTIK_LISTEN__HTTPS"` HTTPS string `yaml:"listen_https" env:"AUTHENTIK_LISTEN__HTTPS"`
LDAP string `yaml:"listen_ldap" env:"AUTHENTIK_LISTEN__LDAP"` LDAP string `yaml:"listen_ldap" env:"AUTHENTIK_LISTEN__LDAP"`
LDAPS string `yaml:"listen_ldaps" env:"AUTHENTIK_LISTEN__LDAPS"` LDAPS string `yaml:"listen_ldaps" env:"AUTHENTIK_LISTEN__LDAPS"`
Radius string `yaml:"listen_radius" env:"AUTHENTIK_LISTEN__RADIUS"` Radius string `yaml:"listen_radius" env:"AUTHENTIK_LISTEN__RADIUS"`
Metrics string `yaml:"listen_metrics" env:"AUTHENTIK_LISTEN__METRICS"` Metrics string `yaml:"listen_metrics" env:"AUTHENTIK_LISTEN__METRICS"`
Debug string `yaml:"listen_debug" env:"AUTHENTIK_LISTEN__DEBUG"` Debug string `yaml:"listen_debug" env:"AUTHENTIK_LISTEN__DEBUG"`
TrustedProxyCIDRs []string `yaml:"trusted_proxy_cidrs" env:"AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS"`
} }
type PathsConfig struct { type PathsConfig struct {

View File

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

View File

@ -0,0 +1,44 @@
package web
import (
"net"
"net/http"
"github.com/gorilla/handlers"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/config"
)
// ProxyHeaders Set proxy headers like X-Forwarded-For and such, but only if the direct connection
// comes from a client that's in a list of trusted CIDRs
func ProxyHeaders() func(http.Handler) http.Handler {
nets := []*net.IPNet{}
for _, rn := range config.Get().Listen.TrustedProxyCIDRs {
_, cidr, err := net.ParseCIDR(rn)
if err != nil {
continue
}
nets = append(nets, cidr)
}
ph := handlers.ProxyHeaders
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
// remoteAddr will be nil if the IP cannot be parsed
remoteAddr := net.ParseIP(host)
for _, allowedCidr := range nets {
if remoteAddr != nil && allowedCidr.Contains(remoteAddr) {
log.WithField("remoteAddr", remoteAddr).WithField("cidr", allowedCidr.String()).Trace("Setting proxy headers")
ph(h).ServeHTTP(w, r)
return
}
}
}
// Request is not directly coming from a CIDR we "trust"
// so set XFF to the direct host IP
r.Header.Set("X-Forwarded-For", host)
h.ServeHTTP(w, r)
})
}
}

View File

@ -35,7 +35,7 @@ type WebServer struct {
func NewWebServer(g *gounicorn.GoUnicorn) *WebServer { func NewWebServer(g *gounicorn.GoUnicorn) *WebServer {
l := log.WithField("logger", "authentik.router") l := log.WithField("logger", "authentik.router")
mainHandler := mux.NewRouter() mainHandler := mux.NewRouter()
mainHandler.Use(handlers.ProxyHeaders) mainHandler.Use(web.ProxyHeaders())
mainHandler.Use(handlers.CompressHandler) mainHandler.Use(handlers.CompressHandler)
loggingHandler := mainHandler.NewRoute().Subrouter() loggingHandler := mainHandler.NewRoute().Subrouter()
loggingHandler.Use(web.NewLoggingHandler(l, nil)) loggingHandler.Use(web.NewLoggingHandler(l, nil))

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-10 17:31+0000\n" "POT-Creation-Date: 2023-05-18 14:21+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -1381,33 +1381,33 @@ msgstr ""
msgid "SCIM Mappings" msgid "SCIM Mappings"
msgstr "" msgstr ""
#: authentik/providers/scim/tasks.py:50 #: authentik/providers/scim/tasks.py:52
msgid "Starting full SCIM sync" msgid "Starting full SCIM sync"
msgstr "" msgstr ""
#: authentik/providers/scim/tasks.py:57 #: authentik/providers/scim/tasks.py:59
#, python-format #, python-format
msgid "Syncing page %(page)d of users" msgid "Syncing page %(page)d of users"
msgstr "" msgstr ""
#: authentik/providers/scim/tasks.py:61 #: authentik/providers/scim/tasks.py:63
#, python-format #, python-format
msgid "Syncing page %(page)d of groups" msgid "Syncing page %(page)d of groups"
msgstr "" msgstr ""
#: authentik/providers/scim/tasks.py:90 #: authentik/providers/scim/tasks.py:92
#, python-format #, python-format
msgid "Failed to sync user due to remote error %(name)s: %(error)s" msgid "Failed to sync user %(user_name)s due to remote error: %(error)s"
msgstr "" msgstr ""
#: authentik/providers/scim/tasks.py:101 authentik/providers/scim/tasks.py:142 #: authentik/providers/scim/tasks.py:103 authentik/providers/scim/tasks.py:144
#, python-format #, python-format
msgid "Stopping sync due to error: %(error)s" msgid "Stopping sync due to error: %(error)s"
msgstr "" msgstr ""
#: authentik/providers/scim/tasks.py:131 #: authentik/providers/scim/tasks.py:133
#, python-format #, python-format
msgid "Failed to sync group due to remote error %(name)s: %(error)s" msgid "Failed to sync group %(group_name)s due to remote error: %(error)s"
msgstr "" msgstr ""
#: authentik/recovery/management/commands/create_admin_group.py:11 #: authentik/recovery/management/commands/create_admin_group.py:11
@ -2106,6 +2106,10 @@ msgid ""
" " " "
msgstr "" msgstr ""
#: authentik/stages/identification/api.py:20
msgid "When no user fields are selected, at least one source must be selected"
msgstr ""
#: authentik/stages/identification/models.py:29 #: authentik/stages/identification/models.py:29
msgid "" msgid ""
"Fields of the user object to match against. (Hold shift to select multiple " "Fields of the user object to match against. (Hold shift to select multiple "
@ -2397,16 +2401,17 @@ msgstr ""
msgid "User Write Stages" msgid "User Write Stages"
msgstr "" msgstr ""
#: authentik/stages/user_write/stage.py:132 #: authentik/stages/user_write/stage.py:133
msgid "No Pending data." msgid "No Pending data."
msgstr "" msgstr ""
#: authentik/stages/user_write/stage.py:138 #: authentik/stages/user_write/stage.py:139
msgid "No user found and can't create new user." msgid "No user found and can't create new user."
msgstr "" msgstr ""
#: authentik/stages/user_write/stage.py:165 #: authentik/stages/user_write/stage.py:156
msgid "Failed to save user" #: authentik/stages/user_write/stage.py:170
msgid "Failed to update user. Please try again later."
msgstr "" msgstr ""
#: authentik/tenants/models.py:23 #: authentik/tenants/models.py:23

110
poetry.lock generated
View File

@ -878,63 +878,63 @@ files = [
[[package]] [[package]]
name = "coverage" name = "coverage"
version = "7.2.5" version = "7.2.6"
description = "Code coverage measurement for Python" description = "Code coverage measurement for Python"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "coverage-7.2.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:883123d0bbe1c136f76b56276074b0c79b5817dd4238097ffa64ac67257f4b6c"}, {file = "coverage-7.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:496b86f1fc9c81a1cd53d8842ef712e950a4611bba0c42d33366a7b91ba969ec"},
{file = "coverage-7.2.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fbc2a127e857d2f8898aaabcc34c37771bf78a4d5e17d3e1f5c30cd0cbc62a"}, {file = "coverage-7.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbe6e8c0a9a7193ba10ee52977d4d5e7652957c1f56ccefed0701db8801a2a3b"},
{file = "coverage-7.2.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f3671662dc4b422b15776cdca89c041a6349b4864a43aa2350b6b0b03bbcc7f"}, {file = "coverage-7.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d06b721c2550c01a60e5d3093f417168658fb454e5dfd9a23570e9bffe39a1"},
{file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780551e47d62095e088f251f5db428473c26db7829884323e56d9c0c3118791a"}, {file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77a04b84d01f0e12c66f16e69e92616442dc675bbe51b90bfb074b1e5d1c7fbd"},
{file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:066b44897c493e0dcbc9e6a6d9f8bbb6607ef82367cf6810d387c09f0cd4fe9a"}, {file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35db06450272473eab4449e9c2ad9bc6a0a68dab8e81a0eae6b50d9c2838767e"},
{file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9a4ee55174b04f6af539218f9f8083140f61a46eabcaa4234f3c2a452c4ed11"}, {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6727a0d929ff0028b1ed8b3e7f8701670b1d7032f219110b55476bb60c390bfb"},
{file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:706ec567267c96717ab9363904d846ec009a48d5f832140b6ad08aad3791b1f5"}, {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aac1d5fdc5378f6bac2c0c7ebe7635a6809f5b4376f6cf5d43243c1917a67087"},
{file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ae453f655640157d76209f42c62c64c4d4f2c7f97256d3567e3b439bd5c9b06c"}, {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c9e4a5eb1bbc3675ee57bc31f8eea4cd7fb0cbcbe4912cf1cb2bf3b754f4a80"},
{file = "coverage-7.2.5-cp310-cp310-win32.whl", hash = "sha256:f81c9b4bd8aa747d417407a7f6f0b1469a43b36a85748145e144ac4e8d303cb5"}, {file = "coverage-7.2.6-cp310-cp310-win32.whl", hash = "sha256:71f739f97f5f80627f1fee2331e63261355fd1e9a9cce0016394b6707ac3f4ec"},
{file = "coverage-7.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:dc945064a8783b86fcce9a0a705abd7db2117d95e340df8a4333f00be5efb64c"}, {file = "coverage-7.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:fde5c7a9d9864d3e07992f66767a9817f24324f354caa3d8129735a3dc74f126"},
{file = "coverage-7.2.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40cc0f91c6cde033da493227797be2826cbf8f388eaa36a0271a97a332bfd7ce"}, {file = "coverage-7.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc7b667f8654376e9353dd93e55e12ce2a59fb6d8e29fce40de682273425e044"},
{file = "coverage-7.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a66e055254a26c82aead7ff420d9fa8dc2da10c82679ea850d8feebf11074d88"}, {file = "coverage-7.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:697f4742aa3f26c107ddcb2b1784a74fe40180014edbd9adaa574eac0529914c"},
{file = "coverage-7.2.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c10fbc8a64aa0f3ed136b0b086b6b577bc64d67d5581acd7cc129af52654384e"}, {file = "coverage-7.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:541280dde49ce74a4262c5e395b48ea1207e78454788887118c421cb4ffbfcac"},
{file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a22cbb5ede6fade0482111fa7f01115ff04039795d7092ed0db43522431b4f2"}, {file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7f1a8328eeec34c54f1d5968a708b50fc38d31e62ca8b0560e84a968fbf9a9"},
{file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292300f76440651529b8ceec283a9370532f4ecba9ad67d120617021bb5ef139"}, {file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bbd58eb5a2371bf160590f4262109f66b6043b0b991930693134cb617bc0169"},
{file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7ff8f3fb38233035028dbc93715551d81eadc110199e14bbbfa01c5c4a43f8d8"}, {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae82c5f168d2a39a5d69a12a69d4dc23837a43cf2ca99be60dfe59996ea6b113"},
{file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a08c7401d0b24e8c2982f4e307124b671c6736d40d1c39e09d7a8687bddf83ed"}, {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f5440cdaf3099e7ab17a5a7065aed59aff8c8b079597b61c1f8be6f32fe60636"},
{file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef9659d1cda9ce9ac9585c045aaa1e59223b143f2407db0eaee0b61a4f266fb6"}, {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a6f03f87fea579d55e0b690d28f5042ec1368650466520fbc400e7aeaf09e995"},
{file = "coverage-7.2.5-cp311-cp311-win32.whl", hash = "sha256:30dcaf05adfa69c2a7b9f7dfd9f60bc8e36b282d7ed25c308ef9e114de7fc23b"}, {file = "coverage-7.2.6-cp311-cp311-win32.whl", hash = "sha256:dc4d5187ef4d53e0d4c8eaf530233685667844c5fb0b855fea71ae659017854b"},
{file = "coverage-7.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:97072cc90f1009386c8a5b7de9d4fc1a9f91ba5ef2146c55c1f005e7b5c5e068"}, {file = "coverage-7.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:c93d52c3dc7b9c65e39473704988602300e3cc1bad08b5ab5b03ca98bbbc68c1"},
{file = "coverage-7.2.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bebea5f5ed41f618797ce3ffb4606c64a5de92e9c3f26d26c2e0aae292f015c1"}, {file = "coverage-7.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42c692b55a647a832025a4c048007034fe77b162b566ad537ce65ad824b12a84"},
{file = "coverage-7.2.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828189fcdda99aae0d6bf718ea766b2e715eabc1868670a0a07bf8404bf58c33"}, {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7786b2fa7809bf835f830779ad285215a04da76293164bb6745796873f0942d"},
{file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e8a95f243d01ba572341c52f89f3acb98a3b6d1d5d830efba86033dd3687ade"}, {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25bad4196104761bc26b1dae9b57383826542ec689ff0042f7f4f4dd7a815cba"},
{file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8834e5f17d89e05697c3c043d3e58a8b19682bf365048837383abfe39adaed5"}, {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2692306d3d4cb32d2cceed1e47cebd6b1d2565c993d6d2eda8e6e6adf53301e6"},
{file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1f25ee9de21a39b3a8516f2c5feb8de248f17da7eead089c2e04aa097936b47"}, {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:392154d09bd4473b9d11351ab5d63391f3d5d24d752f27b3be7498b0ee2b5226"},
{file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1637253b11a18f453e34013c665d8bf15904c9e3c44fbda34c643fbdc9d452cd"}, {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fa079995432037b5e2ef5ddbb270bcd2ded9f52b8e191a5de11fe59a00ea30d8"},
{file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8e575a59315a91ccd00c7757127f6b2488c2f914096077c745c2f1ba5b8c0969"}, {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d712cefff15c712329113b01088ba71bbcef0f7ea58478ca0bbec63a824844cb"},
{file = "coverage-7.2.5-cp37-cp37m-win32.whl", hash = "sha256:509ecd8334c380000d259dc66feb191dd0a93b21f2453faa75f7f9cdcefc0718"}, {file = "coverage-7.2.6-cp37-cp37m-win32.whl", hash = "sha256:004948e296149644d208964300cb3d98affc5211e9e490e9979af4030b0d6473"},
{file = "coverage-7.2.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12580845917b1e59f8a1c2ffa6af6d0908cb39220f3019e36c110c943dc875b0"}, {file = "coverage-7.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:c1d7a31603c3483ac49c1726723b0934f88f2c011c660e6471e7bd735c2fa110"},
{file = "coverage-7.2.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5016e331b75310610c2cf955d9f58a9749943ed5f7b8cfc0bb89c6134ab0a84"}, {file = "coverage-7.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3436927d1794fa6763b89b60c896f9e3bd53212001026ebc9080d23f0c2733c1"},
{file = "coverage-7.2.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:373ea34dca98f2fdb3e5cb33d83b6d801007a8074f992b80311fc589d3e6b790"}, {file = "coverage-7.2.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44c9b9f1a245f3d0d202b1a8fa666a80b5ecbe4ad5d0859c0fb16a52d9763224"},
{file = "coverage-7.2.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a063aad9f7b4c9f9da7b2550eae0a582ffc7623dca1c925e50c3fbde7a579771"}, {file = "coverage-7.2.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3783a286d5a93a2921396d50ce45a909aa8f13eee964465012f110f0cbb611"},
{file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38c0a497a000d50491055805313ed83ddba069353d102ece8aef5d11b5faf045"}, {file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cff6980fe7100242170092bb40d2b1cdad79502cd532fd26b12a2b8a5f9aee0"},
{file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b3b05e22a77bb0ae1a3125126a4e08535961c946b62f30985535ed40e26614"}, {file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c534431153caffc7c495c3eddf7e6a6033e7f81d78385b4e41611b51e8870446"},
{file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0342a28617e63ad15d96dca0f7ae9479a37b7d8a295f749c14f3436ea59fdcb3"}, {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3062fd5c62df988cea9f2972c593f77fed1182bfddc5a3b12b1e606cb7aba99e"},
{file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf97ed82ca986e5c637ea286ba2793c85325b30f869bf64d3009ccc1a31ae3fd"}, {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6284a2005e4f8061c58c814b1600ad0074ccb0289fe61ea709655c5969877b70"},
{file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c2c41c1b1866b670573657d584de413df701f482574bad7e28214a2362cb1fd1"}, {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:97729e6828643f168a2a3f07848e1b1b94a366b13a9f5aba5484c2215724edc8"},
{file = "coverage-7.2.5-cp38-cp38-win32.whl", hash = "sha256:10b15394c13544fce02382360cab54e51a9e0fd1bd61ae9ce012c0d1e103c813"}, {file = "coverage-7.2.6-cp38-cp38-win32.whl", hash = "sha256:dc11b42fa61ff1e788dd095726a0aed6aad9c03d5c5984b54cb9e1e67b276aa5"},
{file = "coverage-7.2.5-cp38-cp38-win_amd64.whl", hash = "sha256:a0b273fe6dc655b110e8dc89b8ec7f1a778d78c9fd9b4bda7c384c8906072212"}, {file = "coverage-7.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:cbcc874f454ee51f158afd604a315f30c0e31dff1d5d5bf499fc529229d964dd"},
{file = "coverage-7.2.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c587f52c81211d4530fa6857884d37f514bcf9453bdeee0ff93eaaf906a5c1b"}, {file = "coverage-7.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d3cacc6a665221108ecdf90517a8028d07a2783df3417d12dcfef1c517e67478"},
{file = "coverage-7.2.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4436cc9ba5414c2c998eaedee5343f49c02ca93b21769c5fdfa4f9d799e84200"}, {file = "coverage-7.2.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:272ab31228a9df857ab5df5d67936d8861464dc89c5d3fab35132626e9369379"},
{file = "coverage-7.2.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6599bf92f33ab041e36e06d25890afbdf12078aacfe1f1d08c713906e49a3fe5"}, {file = "coverage-7.2.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a8723ccec4e564d4b9a79923246f7b9a8de4ec55fa03ec4ec804459dade3c4f"},
{file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:857abe2fa6a4973f8663e039ead8d22215d31db613ace76e4a98f52ec919068e"}, {file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5906f6a84b47f995cd1bf0aca1c72d591c55ee955f98074e93660d64dfc66eb9"},
{file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f5cab2d7f0c12f8187a376cc6582c477d2df91d63f75341307fcdcb5d60303"}, {file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c139b7ab3f0b15f9aad0a3fedef5a1f8c0b2bdc291d88639ca2c97d3682416"},
{file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aa387bd7489f3e1787ff82068b295bcaafbf6f79c3dad3cbc82ef88ce3f48ad3"}, {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a5ffd45c6b93c23a8507e2f436983015c6457aa832496b6a095505ca2f63e8f1"},
{file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:156192e5fd3dbbcb11cd777cc469cf010a294f4c736a2b2c891c77618cb1379a"}, {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4f3c7c19581d471af0e9cb49d928172cd8492cd78a2b7a4e82345d33662929bb"},
{file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd3b4b8175c1db502adf209d06136c000df4d245105c8839e9d0be71c94aefe1"}, {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e8c0e79820cdd67978e1120983786422d279e07a381dbf89d03bbb23ec670a6"},
{file = "coverage-7.2.5-cp39-cp39-win32.whl", hash = "sha256:ddc5a54edb653e9e215f75de377354e2455376f416c4378e1d43b08ec50acc31"}, {file = "coverage-7.2.6-cp39-cp39-win32.whl", hash = "sha256:13cde6bb0e58fb67d09e2f373de3899d1d1e866c5a9ff05d93615f2f54fbd2bb"},
{file = "coverage-7.2.5-cp39-cp39-win_amd64.whl", hash = "sha256:338aa9d9883aaaad53695cb14ccdeb36d4060485bb9388446330bef9c361c252"}, {file = "coverage-7.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:6b9f64526286255735847aed0221b189486e0b9ed943446936e41b7e44b08783"},
{file = "coverage-7.2.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:8877d9b437b35a85c18e3c6499b23674684bf690f5d96c1006a1ef61f9fdf0f3"}, {file = "coverage-7.2.6-pp37.pp38.pp39-none-any.whl", hash = "sha256:6babcbf1e66e46052442f10833cfc4a0d3554d8276aa37af8531a83ed3c1a01d"},
{file = "coverage-7.2.5.tar.gz", hash = "sha256:f99ef080288f09ffc687423b8d60978cf3a465d3f404a18d1a05474bd8575a47"}, {file = "coverage-7.2.6.tar.gz", hash = "sha256:2025f913f2edb0272ef15d00b1f335ff8908c921c8eb2013536fcaf61f5a683d"},
] ]
[package.extras] [package.extras]
@ -3153,14 +3153,14 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "1.23.0" version = "1.23.1"
description = "Python client for Sentry (https://sentry.io)" description = "Python client for Sentry (https://sentry.io)"
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "sentry-sdk-1.23.0.tar.gz", hash = "sha256:58f4ff9e76c21bc7172eeec9f1bccb3ff2247c74c71d5590438ce36c803f46ea"}, {file = "sentry-sdk-1.23.1.tar.gz", hash = "sha256:0300fbe7a07b3865b3885929fb863a68ff01f59e3bcfb4e7953d0bf7fd19c67f"},
{file = "sentry_sdk-1.23.0-py2.py3-none-any.whl", hash = "sha256:01b56a276642d31cf9b4aaf0b55938677265d7006be4785a10ef6330d0f5bba9"}, {file = "sentry_sdk-1.23.1-py2.py3-none-any.whl", hash = "sha256:a884e2478e0b055776ea2b9234d5de9339b4bae0b3a5e74ae43d131db8ded27e"},
] ]
[package.dependencies] [package.dependencies]

View File

@ -113,7 +113,7 @@ filterwarnings = [
[tool.poetry] [tool.poetry]
name = "authentik" name = "authentik"
version = "2023.5.0" version = "2023.5.6"
description = "" description = ""
authors = ["authentik Team <hello@goauthentik.io>"] authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: authentik title: authentik
version: 2023.5.0 version: 2023.5.6
description: Making authentication simple. description: Making authentication simple.
contact: contact:
email: hello@goauthentik.io email: hello@goauthentik.io
@ -4783,6 +4783,38 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/core/users/{id}/impersonate/:
post:
operationId: core_users_impersonate_create
description: Impersonate a user
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this User.
required: true
tags:
- core
security:
- authentik: []
responses:
'204':
description: Successfully started impersonation
'401':
description: Access denied
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/users/{id}/metrics/: /core/users/{id}/metrics/:
get: get:
operationId: core_users_metrics_retrieve operationId: core_users_metrics_retrieve
@ -4962,6 +4994,29 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/core/users/impersonate_end/:
get:
operationId: core_users_impersonate_end_retrieve
description: End Impersonation a user
tags:
- core
security:
- authentik: []
responses:
'204':
description: Successfully started impersonation
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/users/me/: /core/users/me/:
get: get:
operationId: core_users_me_retrieve operationId: core_users_me_retrieve
@ -38956,6 +39011,11 @@ components:
shared_secret: shared_secret:
type: string type: string
description: Shared secret between clients and server to hash packets. description: Shared secret between clients and server to hash packets.
outpost_set:
type: array
items:
type: string
readOnly: true
required: required:
- assigned_application_name - assigned_application_name
- assigned_application_slug - assigned_application_slug
@ -38965,6 +39025,7 @@ components:
- component - component
- meta_model_name - meta_model_name
- name - name
- outpost_set
- pk - pk
- verbose_name - verbose_name
- verbose_name_plural - verbose_name_plural
@ -39824,11 +39885,11 @@ components:
type: string type: string
description: Get object component so that we know how to edit the object description: Get object component so that we know how to edit the object
readOnly: true readOnly: true
assigned_application_slug: assigned_backchannel_application_slug:
type: string type: string
description: Internal application name, used in URLs. description: Internal application name, used in URLs.
readOnly: true readOnly: true
assigned_application_name: assigned_backchannel_application_name:
type: string type: string
description: Application's display Name. description: Application's display Name.
readOnly: true readOnly: true
@ -39857,8 +39918,8 @@ components:
format: uuid format: uuid
nullable: true nullable: true
required: required:
- assigned_application_name - assigned_backchannel_application_name
- assigned_application_slug - assigned_backchannel_application_slug
- component - component
- meta_model_name - meta_model_name
- name - name
@ -40487,12 +40548,6 @@ components:
type: object type: object
description: Get system information. description: Get system information.
properties: properties:
env:
type: object
additionalProperties:
type: string
description: Get Environment
readOnly: true
http_headers: http_headers:
type: object type: object
additionalProperties: additionalProperties:
@ -40546,7 +40601,6 @@ components:
readOnly: true readOnly: true
required: required:
- embedded_outpost_host - embedded_outpost_host
- env
- http_headers - http_headers
- http_host - http_host
- http_is_secure - http_is_secure
@ -40971,7 +41025,6 @@ components:
type: string type: string
required: required:
- avatar - avatar
- groups
- groups_obj - groups_obj
- is_superuser - is_superuser
- name - name
@ -41429,7 +41482,6 @@ components:
type: string type: string
minLength: 1 minLength: 1
required: required:
- groups
- name - name
- username - username
UserSAMLSourceConnection: UserSAMLSourceConnection:

View File

@ -243,7 +243,7 @@ class TestSourceOAuth1(SeleniumTestCase):
def get_container_specs(self) -> Optional[dict[str, Any]]: def get_container_specs(self) -> Optional[dict[str, Any]]:
return { return {
"image": "ghcr.io/beryju/oauth1-test-server:latest", "image": "ghcr.io/beryju/oauth1-test-server:v1.1",
"detach": True, "detach": True,
"network_mode": "host", "network_mode": "host",
"auto_remove": True, "auto_remove": True,

138
web/package-lock.json generated
View File

@ -17,12 +17,12 @@
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-listformat": "^7.2.2",
"@fortawesome/fontawesome-free": "^6.4.0", "@fortawesome/fontawesome-free": "^6.4.0",
"@goauthentik/api": "^2023.4.1-1683668555", "@goauthentik/api": "^2023.5.3-1687462221",
"@lingui/cli": "^4.1.0", "@lingui/cli": "^4.1.2",
"@lingui/core": "^4.1.0", "@lingui/core": "^4.1.2",
"@lingui/detect-locale": "^4.1.0", "@lingui/detect-locale": "^4.1.2",
"@lingui/format-po-gettext": "^4.1.0", "@lingui/format-po-gettext": "^4.1.2",
"@lingui/macro": "^4.1.0", "@lingui/macro": "^4.1.2",
"@patternfly/patternfly": "^4.224.2", "@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.52.1", "@sentry/browser": "^7.52.1",
"@sentry/tracing": "^7.52.1", "@sentry/tracing": "^7.52.1",
@ -75,7 +75,7 @@
"rollup-plugin-minify-html-literals": "^1.2.6", "rollup-plugin-minify-html-literals": "^1.2.6",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"ts-lit-plugin": "^1.2.1", "ts-lit-plugin": "^1.2.1",
"tslib": "^2.5.0", "tslib": "^2.5.1",
"turnstile-types": "^1.1.2", "turnstile-types": "^1.1.2",
"typescript": "^5.0.4" "typescript": "^5.0.4"
} }
@ -2127,9 +2127,9 @@
} }
}, },
"node_modules/@goauthentik/api": { "node_modules/@goauthentik/api": {
"version": "2023.4.1-1683802980", "version": "2023.5.3-1687462221",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.4.1-1683802980.tgz", "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.5.3-1687462221.tgz",
"integrity": "sha512-eHjzIS3yv1jmk6DXof1GoODizBWHHCJiz+3/qZ2LqG60nkvIm/k0pF3hjw4jv+aZ5GiXzFa+/K/YpUwtGDv+hw==" "integrity": "sha512-34LJCBVPOfdlIHhDPQEA7NS7mJvrKJKHSfa2HPQClyVM3o5us8Bp4yJKs2nm4hGime3rbZAwYYq7n75tccr7rQ=="
}, },
"node_modules/@hcaptcha/types": { "node_modules/@hcaptcha/types": {
"version": "1.0.3", "version": "1.0.3",
@ -2411,28 +2411,28 @@
} }
}, },
"node_modules/@lingui/babel-plugin-extract-messages": { "node_modules/@lingui/babel-plugin-extract-messages": {
"version": "4.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-4.1.2.tgz",
"integrity": "sha512-g/ZMnsO8UEA9QTukYOFC92Tfnhg535MTgxLKNzzulomz+YorCdos5EzpffF9MopNL35nSv8rJNrRVWOrIVyrCw==", "integrity": "sha512-FhdfV9XS3MUkQkmYK6SC4q6i2qQhk3HfVG5bhThukB8dHn6iK0sytBK9uL7BLsV4TJR6YKi3mDTO4MMWreYHHw==",
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/@lingui/cli": { "node_modules/@lingui/cli": {
"version": "4.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@lingui/cli/-/cli-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@lingui/cli/-/cli-4.1.2.tgz",
"integrity": "sha512-n5lanpFazZ8+5U2ymAiA+eWQXqxKtjq8t/lC3307uUD6GChJnkzEknreJR58H1b3XhHjZY5uMPqBixI8zWsYrg==", "integrity": "sha512-5xAz+YZ45g5+Qt12GprVcglRjx7Us2XQmUzmO/GVIckIEXq8CAtKVdyveuvtpVxk2GLH30HCGtfIiPfvV11gVw==",
"dependencies": { "dependencies": {
"@babel/core": "^7.21.0", "@babel/core": "^7.21.0",
"@babel/generator": "^7.21.1", "@babel/generator": "^7.21.1",
"@babel/parser": "^7.21.2", "@babel/parser": "^7.21.2",
"@babel/runtime": "^7.21.0", "@babel/runtime": "^7.21.0",
"@babel/types": "^7.21.2", "@babel/types": "^7.21.2",
"@lingui/babel-plugin-extract-messages": "4.1.0", "@lingui/babel-plugin-extract-messages": "4.1.2",
"@lingui/conf": "4.1.0", "@lingui/conf": "4.1.2",
"@lingui/core": "4.1.0", "@lingui/core": "4.1.2",
"@lingui/format-po": "4.1.0", "@lingui/format-po": "4.1.2",
"@lingui/message-utils": "4.1.0", "@lingui/message-utils": "4.1.2",
"babel-plugin-macros": "^3.0.1", "babel-plugin-macros": "^3.0.1",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"chokidar": "3.5.1", "chokidar": "3.5.1",
@ -2446,6 +2446,7 @@
"micromatch": "4.0.2", "micromatch": "4.0.2",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"ora": "^5.1.0", "ora": "^5.1.0",
"pathe": "^1.1.0",
"pkg-up": "^3.1.0", "pkg-up": "^3.1.0",
"pofile": "^1.1.4", "pofile": "^1.1.4",
"pseudolocale": "^2.0.0", "pseudolocale": "^2.0.0",
@ -2460,9 +2461,9 @@
} }
}, },
"node_modules/@lingui/cli/node_modules/@lingui/message-utils": { "node_modules/@lingui/cli/node_modules/@lingui/message-utils": {
"version": "4.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.1.2.tgz",
"integrity": "sha512-R+ilZj1C7g/bgLnuEGpW22fy6vGWf0VgrGCChs+2gGagNkow/a5a8lSzotm7qzVNE1t21DWDYSPFaiYAbsFgRw==", "integrity": "sha512-gN2tCxr05cnIswlYMn4TN1TyD//TPplZwbIH2OIW+qpZyFjTQFqPq+OnlNcDIIFdVm3G82cF/fm84LrFhRfTTQ==",
"dependencies": { "dependencies": {
"@messageformat/parser": "^5.0.0" "@messageformat/parser": "^5.0.0"
}, },
@ -2574,9 +2575,9 @@
} }
}, },
"node_modules/@lingui/conf": { "node_modules/@lingui/conf": {
"version": "4.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@lingui/conf/-/conf-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@lingui/conf/-/conf-4.1.2.tgz",
"integrity": "sha512-khPxgyCJ562iLDn030n1HmLIMZ2vDeENGjorffosY1Bew7O0uREkwhPquNiP4PpTsT4S4W/102XPTVrKMdmiSQ==", "integrity": "sha512-20a4wwnFa+2NHnSentNR8mFTnp2F3zo7/n3FJO5cDQl2oh77Xfb8U3epLAAkoz6wDRLVqTXNlelyCr6+fgVGGA==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"chalk": "^4.1.0", "chalk": "^4.1.0",
@ -2671,21 +2672,21 @@
} }
}, },
"node_modules/@lingui/core": { "node_modules/@lingui/core": {
"version": "4.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@lingui/core/-/core-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@lingui/core/-/core-4.1.2.tgz",
"integrity": "sha512-w9/NTWC1koTXbiBgznkIXJ26dmvORLvVU92sIb8taFardH8Y+bYNlMSZczY+jfT6/BokgmHkBBLISKj1YyKq2Q==", "integrity": "sha512-d53SYWsBcLvQZh62aEZFq+FJbHi+1CopoNzP5U2Q1xrhbNWNcfE7jbp86XZJICzNbFXDLoU4aCVMMg8p/JBzkw==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@lingui/message-utils": "4.1.0" "@lingui/message-utils": "4.1.2"
}, },
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/@lingui/core/node_modules/@lingui/message-utils": { "node_modules/@lingui/core/node_modules/@lingui/message-utils": {
"version": "4.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.1.2.tgz",
"integrity": "sha512-R+ilZj1C7g/bgLnuEGpW22fy6vGWf0VgrGCChs+2gGagNkow/a5a8lSzotm7qzVNE1t21DWDYSPFaiYAbsFgRw==", "integrity": "sha512-gN2tCxr05cnIswlYMn4TN1TyD//TPplZwbIH2OIW+qpZyFjTQFqPq+OnlNcDIIFdVm3G82cF/fm84LrFhRfTTQ==",
"dependencies": { "dependencies": {
"@messageformat/parser": "^5.0.0" "@messageformat/parser": "^5.0.0"
}, },
@ -2694,20 +2695,20 @@
} }
}, },
"node_modules/@lingui/detect-locale": { "node_modules/@lingui/detect-locale": {
"version": "4.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@lingui/detect-locale/-/detect-locale-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@lingui/detect-locale/-/detect-locale-4.1.2.tgz",
"integrity": "sha512-ip1puEy2UXfws+O54mht+kyttX/RwavQie37DesT9oYAp8W/qWg2PHoynwbp8EA0iR6yv00JlZQiINgMZN01eA==", "integrity": "sha512-G/bh1gxT+RnhPp+ccGXDWaw/gx39wOM0jyo/oOGPp2cqTEdM4S5R00Rgalh3aOcYnF7hEPBvEk0wrgAqAt+7pw==",
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/@lingui/format-po": { "node_modules/@lingui/format-po": {
"version": "4.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@lingui/format-po/-/format-po-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@lingui/format-po/-/format-po-4.1.2.tgz",
"integrity": "sha512-aFG71XmZXEhoZKapE77SI595E/kqy75owE6XFxTUntz2UCWMY5uUKS4p4ogei/1pWTEQAk26MX/BbYaxkKDu9g==", "integrity": "sha512-zYDJ9ZK3cY4uTuS7q+bViEW1oSIao/vd7Y49vwi8+OAHalddkCBWjlMs+Cx5lFJ9qoIm+x/jAN0PjIFhsl3DJA==",
"dependencies": { "dependencies": {
"@lingui/conf": "4.1.0", "@lingui/conf": "4.1.2",
"@lingui/message-utils": "4.1.0", "@lingui/message-utils": "4.1.2",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"pofile": "^1.1.4" "pofile": "^1.1.4"
}, },
@ -2716,13 +2717,13 @@
} }
}, },
"node_modules/@lingui/format-po-gettext": { "node_modules/@lingui/format-po-gettext": {
"version": "4.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@lingui/format-po-gettext/-/format-po-gettext-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@lingui/format-po-gettext/-/format-po-gettext-4.1.2.tgz",
"integrity": "sha512-Xyo/97mQwzeI0ipKjZ/DIa1ZojDEVjtKRcP6F6kFDaKsGg4UCNLuMHdIBYN5xztjTbcPZ+NT7P3ibl5XkwgykA==", "integrity": "sha512-gdsXE677VhGvWJ8QORTS9SuKPMd0SuhBcnpSzT9j3lqDtL/YdBNYQfKzvF+HJe9dwtv8plThHDMBzW1rTAYmsQ==",
"dependencies": { "dependencies": {
"@lingui/conf": "4.1.0", "@lingui/conf": "4.1.2",
"@lingui/format-po": "4.1.0", "@lingui/format-po": "4.1.2",
"@lingui/message-utils": "4.1.0", "@lingui/message-utils": "4.1.2",
"@messageformat/parser": "^5.0.0", "@messageformat/parser": "^5.0.0",
"node-gettext": "^3.0.0", "node-gettext": "^3.0.0",
"plurals-cldr": "^2.0.1", "plurals-cldr": "^2.0.1",
@ -2733,9 +2734,9 @@
} }
}, },
"node_modules/@lingui/format-po-gettext/node_modules/@lingui/message-utils": { "node_modules/@lingui/format-po-gettext/node_modules/@lingui/message-utils": {
"version": "4.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.1.2.tgz",
"integrity": "sha512-R+ilZj1C7g/bgLnuEGpW22fy6vGWf0VgrGCChs+2gGagNkow/a5a8lSzotm7qzVNE1t21DWDYSPFaiYAbsFgRw==", "integrity": "sha512-gN2tCxr05cnIswlYMn4TN1TyD//TPplZwbIH2OIW+qpZyFjTQFqPq+OnlNcDIIFdVm3G82cF/fm84LrFhRfTTQ==",
"dependencies": { "dependencies": {
"@messageformat/parser": "^5.0.0" "@messageformat/parser": "^5.0.0"
}, },
@ -2744,9 +2745,9 @@
} }
}, },
"node_modules/@lingui/format-po/node_modules/@lingui/message-utils": { "node_modules/@lingui/format-po/node_modules/@lingui/message-utils": {
"version": "4.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.1.2.tgz",
"integrity": "sha512-R+ilZj1C7g/bgLnuEGpW22fy6vGWf0VgrGCChs+2gGagNkow/a5a8lSzotm7qzVNE1t21DWDYSPFaiYAbsFgRw==", "integrity": "sha512-gN2tCxr05cnIswlYMn4TN1TyD//TPplZwbIH2OIW+qpZyFjTQFqPq+OnlNcDIIFdVm3G82cF/fm84LrFhRfTTQ==",
"dependencies": { "dependencies": {
"@messageformat/parser": "^5.0.0" "@messageformat/parser": "^5.0.0"
}, },
@ -2755,15 +2756,15 @@
} }
}, },
"node_modules/@lingui/macro": { "node_modules/@lingui/macro": {
"version": "4.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@lingui/macro/-/macro-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@lingui/macro/-/macro-4.1.2.tgz",
"integrity": "sha512-4EPJGvQSiKQ8fu2tQU840VJaHRRi4t15MhuOLRi/pmAHEcKfBPhxy8s9w5ifCzgGUe5CB+nJYOLpRKxJT3YErA==", "integrity": "sha512-Q0BwweK3L+9Pd+CwiDbQa0tXHDQzQjK0hbimz5WLMXWkWUPjuQPyereuWITFf2sK1TuTVq2YsrV3Qorlc3j5FQ==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@babel/types": "^7.20.7", "@babel/types": "^7.20.7",
"@lingui/conf": "4.1.0", "@lingui/conf": "4.1.2",
"@lingui/core": "4.1.0", "@lingui/core": "4.1.2",
"@lingui/message-utils": "4.1.0" "@lingui/message-utils": "4.1.2"
}, },
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.0.0"
@ -2774,9 +2775,9 @@
} }
}, },
"node_modules/@lingui/macro/node_modules/@lingui/message-utils": { "node_modules/@lingui/macro/node_modules/@lingui/message-utils": {
"version": "4.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.1.2.tgz",
"integrity": "sha512-R+ilZj1C7g/bgLnuEGpW22fy6vGWf0VgrGCChs+2gGagNkow/a5a8lSzotm7qzVNE1t21DWDYSPFaiYAbsFgRw==", "integrity": "sha512-gN2tCxr05cnIswlYMn4TN1TyD//TPplZwbIH2OIW+qpZyFjTQFqPq+OnlNcDIIFdVm3G82cF/fm84LrFhRfTTQ==",
"dependencies": { "dependencies": {
"@messageformat/parser": "^5.0.0" "@messageformat/parser": "^5.0.0"
}, },
@ -8420,6 +8421,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/pathe": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz",
"integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w=="
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -10010,9 +10016,9 @@
"dev": true "dev": true
}, },
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.1.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" "integrity": "sha512-KaI6gPil5m9vF7DKaoXxx1ia9fxS4qG5YveErRRVknPDXXriu5M8h48YRjB6h5ZUOKuAKlSJYb0GaDe8I39fRw=="
}, },
"node_modules/tsutils": { "node_modules/tsutils": {
"version": "3.21.0", "version": "3.21.0",

View File

@ -23,13 +23,13 @@
"@codemirror/legacy-modes": "^6.3.2", "@codemirror/legacy-modes": "^6.3.2",
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-listformat": "^7.2.2",
"@goauthentik/api": "^2023.4.1-1683668555",
"@fortawesome/fontawesome-free": "^6.4.0", "@fortawesome/fontawesome-free": "^6.4.0",
"@lingui/cli": "^4.1.0", "@goauthentik/api": "^2023.5.3-1687462221",
"@lingui/core": "^4.1.0", "@lingui/cli": "^4.1.2",
"@lingui/detect-locale": "^4.1.0", "@lingui/core": "^4.1.2",
"@lingui/format-po-gettext": "^4.1.0", "@lingui/detect-locale": "^4.1.2",
"@lingui/macro": "^4.1.0", "@lingui/format-po-gettext": "^4.1.2",
"@lingui/macro": "^4.1.2",
"@patternfly/patternfly": "^4.224.2", "@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.52.1", "@sentry/browser": "^7.52.1",
"@sentry/tracing": "^7.52.1", "@sentry/tracing": "^7.52.1",
@ -82,7 +82,7 @@
"rollup-plugin-minify-html-literals": "^1.2.6", "rollup-plugin-minify-html-literals": "^1.2.6",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"ts-lit-plugin": "^1.2.1", "ts-lit-plugin": "^1.2.1",
"tslib": "^2.5.0", "tslib": "^2.5.1",
"turnstile-types": "^1.1.2", "turnstile-types": "^1.1.2",
"typescript": "^5.0.4" "typescript": "^5.0.4"
} }

View File

@ -31,7 +31,7 @@ import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css"; import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { AdminApi, SessionUser, Version } from "@goauthentik/api"; import { AdminApi, CoreApi, SessionUser, Version } from "@goauthentik/api";
autoDetectLanguage(); autoDetectLanguage();
@ -175,10 +175,11 @@ export class AdminInterface extends Interface {
${this.user?.original ${this.user?.original
? html`<ak-sidebar-item ? html`<ak-sidebar-item
?highlight=${true} ?highlight=${true}
?isAbsoluteLink=${true} @click=${() => {
path=${`/-/impersonation/end/?back=${encodeURIComponent( new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
`${window.location.pathname}#${window.location.hash}`, window.location.reload();
)}`} });
}}
> >
<span slot="label" <span slot="label"
>${t`You're currently impersonating ${this.user.user.username}. Click to stop.`}</span >${t`You're currently impersonating ${this.user.user.username}. Click to stop.`}</span

View File

@ -115,9 +115,8 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
`; `;
} }
renderForm(): TemplateResult { renderInlineForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser">
<ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser">
<ak-search-select <ak-search-select
.fetchObjects=${async (query?: string): Promise<User[]> => { .fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = { const args: CoreUsersListRequest = {
@ -144,7 +143,6 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
> >
</ak-search-select> </ak-search-select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${this.result ? this.renderResult() : html``} ${this.result ? this.renderResult() : html``}`;
</form>`;
} }
} }

View File

@ -21,9 +21,12 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
}); });
} }
renderForm(): TemplateResult { renderInlineForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<ak-form-element-horizontal
<ak-form-element-horizontal label=${t`Common Name`} name="commonName" ?required=${true}> label=${t`Common Name`}
name="commonName"
?required=${true}
>
<input type="text" class="pf-c-form-control" required /> <input type="text" class="pf-c-form-control" required />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Subject-alt name`} name="subjectAltName"> <ak-form-element-horizontal label=${t`Subject-alt name`} name="subjectAltName">
@ -38,7 +41,6 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
?required=${true} ?required=${true}
> >
<input class="pf-c-form-control" type="number" value="365" /> <input class="pf-c-form-control" type="number" value="365" />
</ak-form-element-horizontal> </ak-form-element-horizontal>`;
</form>`;
} }
} }

View File

@ -87,15 +87,13 @@ export class FlowImportForm extends Form<Flow> {
`; `;
} }
renderForm(): TemplateResult { renderInlineForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<ak-form-element-horizontal label=${t`Flow`} name="flow">
<ak-form-element-horizontal label=${t`Flow`} name="flow">
<input type="file" value="" class="pf-c-form-control" /> <input type="file" value="" class="pf-c-form-control" />
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`.yaml files, which can be found on goauthentik.io and can be exported by authentik.`} ${t`.yaml files, which can be found on goauthentik.io and can be exported by authentik.`}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${this.result ? this.renderResult() : html``} ${this.result ? this.renderResult() : html``}`;
</form>`;
} }
} }

View File

@ -46,41 +46,39 @@ export class RelatedGroupAdd extends Form<{ groups: string[] }> {
return data; return data;
} }
renderForm(): TemplateResult { renderInlineForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<ak-form-element-horizontal label=${t`Groups to add`} name="groups">
<ak-form-element-horizontal label=${t`Groups to add`} name="groups"> <div class="pf-c-input-group">
<div class="pf-c-input-group"> <ak-user-group-select-table
<ak-user-group-select-table .confirm=${(items: Group[]) => {
.confirm=${(items: Group[]) => { this.groupsToAdd = items;
this.groupsToAdd = items; this.requestUpdate();
this.requestUpdate(); return Promise.resolve();
return Promise.resolve(); }}
}} >
> <button slot="trigger" class="pf-c-button pf-m-control" type="button">
<button slot="trigger" class="pf-c-button pf-m-control" type="button"> <i class="fas fa-plus" aria-hidden="true"></i>
<i class="fas fa-plus" aria-hidden="true"></i> </button>
</button> </ak-user-group-select-table>
</ak-user-group-select-table> <div class="pf-c-form-control">
<div class="pf-c-form-control"> <ak-chip-group>
<ak-chip-group> ${this.groupsToAdd.map((group) => {
${this.groupsToAdd.map((group) => { return html`<ak-chip
return html`<ak-chip .removable=${true}
.removable=${true} value=${ifDefined(group.pk)}
value=${ifDefined(group.pk)} @remove=${() => {
@remove=${() => { const idx = this.groupsToAdd.indexOf(group);
const idx = this.groupsToAdd.indexOf(group); this.groupsToAdd.splice(idx, 1);
this.groupsToAdd.splice(idx, 1); this.requestUpdate();
this.requestUpdate(); }}
}} >
> ${group.name}
${group.name} </ak-chip>`;
</ak-chip>`; })}
})} </ak-chip-group>
</ak-chip-group>
</div>
</div> </div>
</ak-form-element-horizontal> </div>
</form> `; </ak-form-element-horizontal>`;
} }
} }

View File

@ -191,8 +191,12 @@ export class OutpostForm extends ModelForm<Outpost, string> {
const selected = Array.from(this.instance?.providers || []).some((sp) => { const selected = Array.from(this.instance?.providers || []).some((sp) => {
return sp == provider.pk; return sp == provider.pk;
}); });
let appName = provider.assignedApplicationName;
if (provider.assignedBackchannelApplicationName) {
appName = provider.assignedBackchannelApplicationName;
}
return html`<option value=${ifDefined(provider.pk)} ?selected=${selected}> return html`<option value=${ifDefined(provider.pk)} ?selected=${selected}>
${provider.assignedApplicationName} (${provider.name}) ${appName} (${provider.name})
</option>`; </option>`;
})} })}
</select> </select>

View File

@ -116,9 +116,8 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
`; `;
} }
renderForm(): TemplateResult { renderInlineForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
<ak-search-select <ak-search-select
.fetchObjects=${async (query?: string): Promise<User[]> => { .fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = { const args: CoreUsersListRequest = {
@ -155,7 +154,6 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
${t`Set custom attributes using YAML or JSON.`} ${t`Set custom attributes using YAML or JSON.`}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${this.result ? this.renderResult() : html``} ${this.result ? this.renderResult() : html``}`;
</form>`;
} }
} }

View File

@ -119,9 +119,8 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
`; `;
} }
renderForm(): TemplateResult { renderInlineForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
<ak-search-select <ak-search-select
.fetchObjects=${async (query?: string): Promise<User[]> => { .fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = { const args: CoreUsersListRequest = {
@ -156,7 +155,6 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
</ak-codemirror> </ak-codemirror>
<p class="pf-c-form__helper-text">${this.renderExampleButtons()}</p> <p class="pf-c-form__helper-text">${this.renderExampleButtons()}</p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${this.result ? this.renderResult() : html``} ${this.result ? this.renderResult() : html``}`;
</form>`;
} }
} }

View File

@ -61,6 +61,10 @@ export class ProviderViewPage extends AKElement {
return html`<ak-provider-scim-view return html`<ak-provider-scim-view
providerID=${ifDefined(this.provider.pk)} providerID=${ifDefined(this.provider.pk)}
></ak-provider-scim-view>`; ></ak-provider-scim-view>`;
case "ak-provider-radius-form":
return html`<ak-provider-radius-view
providerID=${ifDefined(this.provider.pk)}
></ak-provider-radius-view>`;
default: default:
return html`<p>Invalid provider type ${this.provider?.component}</p>`; return html`<p>Invalid provider type ${this.provider?.component}</p>`;
} }

View File

@ -79,6 +79,11 @@ export class RadiusProviderViewPage extends AKElement {
data-tab-title="${t`Overview`}" data-tab-title="${t`Overview`}"
class="pf-c-page__main-section pf-m-no-padding-mobile" class="pf-c-page__main-section pf-m-no-padding-mobile"
> >
${this.provider?.outpostSet.length < 1
? html`<div slot="header" class="pf-c-banner pf-m-warning">
${t`Warning: Provider is not used by any Outpost.`}
</div>`
: html``}
<div class="pf-u-display-flex pf-u-justify-content-center"> <div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75"> <div class="pf-u-w-75">
<div class="pf-c-card"> <div class="pf-c-card">
@ -152,7 +157,7 @@ export class RadiusProviderViewPage extends AKElement {
<ak-object-changelog <ak-object-changelog
targetModelPk=${this.provider.pk || ""} targetModelPk=${this.provider.pk || ""}
targetModelApp="authentik_providers_radius" targetModelApp="authentik_providers_radius"
targetModelName="RadiusProvider" targetModelName="radiusprovider"
> >
</ak-object-changelog> </ak-object-changelog>
</div> </div>

View File

@ -37,9 +37,8 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
}); });
} }
renderForm(): TemplateResult { renderInlineForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
<input type="text" class="pf-c-form-control" required /> <input type="text" class="pf-c-form-control" required />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
@ -77,7 +76,6 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
<ak-form-element-horizontal label=${t`Metadata`} name="metadata"> <ak-form-element-horizontal label=${t`Metadata`} name="metadata">
<input type="file" value="" class="pf-c-form-control" /> <input type="file" value="" class="pf-c-form-control" />
</ak-form-element-horizontal> </ak-form-element-horizontal>`;
</form>`;
} }
} }

View File

@ -121,9 +121,14 @@ export class SCIMProviderViewPage extends AKElement {
if (!this.provider) { if (!this.provider) {
return html``; return html``;
} }
return html` <div slot="header" class="pf-c-banner pf-m-info"> return html`<div slot="header" class="pf-c-banner pf-m-info">
${t`SCIM provider is in preview.`} ${t`SCIM provider is in preview.`}
</div> </div>
${!this.provider?.assignedBackchannelApplicationName
? html`<div slot="header" class="pf-c-banner pf-m-warning">
${t`Warning: Provider is not assigned to an application as backchannel provider.`}
</div>`
: html``}
<div class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"> <div class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
<div class="pf-l-grid__item pf-m-7-col pf-l-stack pf-m-gutter"> <div class="pf-l-grid__item pf-m-7-col pf-l-stack pf-m-gutter">
<div class="pf-c-card pf-m-12-col pf-l-stack__item"> <div class="pf-c-card pf-m-12-col pf-l-stack__item">

View File

@ -59,9 +59,8 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
return data; return data;
} }
renderForm(): TemplateResult { renderInlineForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`${this.group?.isSuperuser ? html`` : html``}
${this.group?.isSuperuser ? html`` : html``}
<ak-form-element-horizontal label=${t`Users to add`} name="users"> <ak-form-element-horizontal label=${t`Users to add`} name="users">
<div class="pf-c-input-group"> <div class="pf-c-input-group">
<ak-group-member-select-table <ak-group-member-select-table
@ -93,8 +92,7 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
</ak-chip-group> </ak-chip-group>
</div> </div>
</div> </div>
</ak-form-element-horizontal> </ak-form-element-horizontal>`;
</form> `;
} }
} }
@ -193,12 +191,20 @@ export class RelatedUserList extends Table<User> {
</ak-forms-modal> </ak-forms-modal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
? html` ? html`
<a <ak-action-button
class="pf-c-button pf-m-tertiary" class="pf-m-tertiary"
href="${`/-/impersonation/${item.pk}/`}" .apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateCreate({
id: item.pk,
})
.then(() => {
window.location.href = "/";
});
}}
> >
${t`Impersonate`} ${t`Impersonate`}
</a> </ak-action-button>
` `
: html``}`, : html``}`,
]; ];

View File

@ -35,9 +35,8 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
this.result = undefined; this.result = undefined;
} }
renderRequestForm(): TemplateResult { renderInlineForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="name">
<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="name">
<input type="text" value="" class="pf-c-form-control" required /> <input type="text" value="" class="pf-c-form-control" required />
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`User's primary identifier. 150 characters or fewer.`} ${t`User's primary identifier. 150 characters or fewer.`}
@ -78,8 +77,7 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
value="${dateTimeLocal(new Date(Date.now() + 1000 * 60 ** 2 * 24 * 360))}" value="${dateTimeLocal(new Date(Date.now() + 1000 * 60 ** 2 * 24 * 360))}"
class="pf-c-form-control" class="pf-c-form-control"
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>`;
</form>`;
} }
renderResponseForm(): TemplateResult { renderResponseForm(): TemplateResult {
@ -113,6 +111,6 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
if (this.result) { if (this.result) {
return this.renderResponseForm(); return this.renderResponseForm();
} }
return this.renderRequestForm(); return super.renderForm();
} }
} }

View File

@ -196,12 +196,20 @@ export class UserListPage extends TablePage<User> {
</ak-forms-modal> </ak-forms-modal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
? html` ? html`
<a <ak-action-button
class="pf-c-button pf-m-tertiary" class="pf-m-tertiary"
href="${`/-/impersonation/${item.pk}/`}" .apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateCreate({
id: item.pk,
})
.then(() => {
window.location.href = "/";
});
}}
> >
${t`Impersonate`} ${t`Impersonate`}
</a> </ak-action-button>
` `
: html``}`, : html``}`,
]; ];

View File

@ -26,11 +26,13 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
}); });
} }
renderForm(): TemplateResult { renderInlineForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<ak-form-element-horizontal
<ak-form-element-horizontal label=${t`Password`} ?required=${true} name="password"> label=${t`Password`}
<input type="password" value="" class="pf-c-form-control" required /> ?required=${true}
</ak-form-element-horizontal> name="password"
</form>`; >
<input type="password" value="" class="pf-c-form-control" required />
</ak-form-element-horizontal>`;
} }
} }

View File

@ -32,32 +32,34 @@ export class UserResetEmailForm extends Form<CoreUsersRecoveryEmailRetrieveReque
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailRetrieve(data); return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailRetrieve(data);
} }
renderForm(): TemplateResult { renderInlineForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<ak-form-element-horizontal
<ak-form-element-horizontal label=${t`Email stage`} ?required=${true} name="emailStage"> label=${t`Email stage`}
<ak-search-select ?required=${true}
.fetchObjects=${async (query?: string): Promise<Stage[]> => { name="emailStage"
const args: StagesAllListRequest = { >
ordering: "name", <ak-search-select
}; .fetchObjects=${async (query?: string): Promise<Stage[]> => {
if (query !== undefined) { const args: StagesAllListRequest = {
args.search = query; ordering: "name",
} };
const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args); if (query !== undefined) {
return stages.results; args.search = query;
}} }
.groupBy=${(items: Stage[]) => { const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
return groupBy(items, (stage) => stage.verboseNamePlural); return stages.results;
}} }}
.renderElement=${(stage: Stage): string => { .groupBy=${(items: Stage[]) => {
return stage.name; return groupBy(items, (stage) => stage.verboseNamePlural);
}} }}
.value=${(stage: Stage | undefined): string | undefined => { .renderElement=${(stage: Stage): string => {
return stage?.pk; return stage.name;
}} }}
> .value=${(stage: Stage | undefined): string | undefined => {
</ak-search-select> return stage?.pk;
</ak-form-element-horizontal> }}
</form>`; >
</ak-search-select>
</ak-form-element-horizontal>`;
} }
} }

View File

@ -201,12 +201,20 @@ export class UserViewPage extends AKElement {
) )
? html` ? html`
<div class="pf-c-card__footer"> <div class="pf-c-card__footer">
<a <ak-action-button
class="pf-c-button pf-m-tertiary" class="pf-m-tertiary"
href="${`/-/impersonation/${this.user?.pk}/`}" .apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateCreate({
id: this.user?.pk || 0,
})
.then(() => {
window.location.href = "/";
});
}}
> >
${t`Impersonate`} ${t`Impersonate`}
</a> </ak-action-button>
</div> </div>
` `
: html``} : html``}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 KiB

After

Width:  |  Height:  |  Size: 399 KiB

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger"; export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress"; export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current"; export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2023.5.0"; export const VERSION = "2023.5.6";
export const TITLE_DEFAULT = "authentik"; export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";"; export const ROUTE_SEPARATOR = ";";

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