Compare commits

..

198 Commits

Author SHA1 Message Date
72f85defb8 release: 2023.3.0 2023-03-13 18:30:48 +01:00
b46048e74f website/docs: final 2023.3 release notes (#4923)
* website/docs: final 2023.3 release notes

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

* Apply suggestions from code review

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 L. <jens@beryju.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2023-03-13 18:30:11 +01:00
bf7dc5df78 website/docs: separate pages for each webserver (#4911)
* website/docs: separate pages for each webserver

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

* Apply suggestions from code review

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 L. <jens@beryju.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2023-03-13 17:29:51 +01:00
f0d0abb66e website/integrations: Update Skyhigh provider instructions (#4921)
Update Skyhigh Provider instructions

Co-authored-by: Nate Brady <nate@skyhighsecurity.com>
2023-03-13 15:44:46 +01:00
fab6a8f8c9 stages/user_login: expiry before login (#4920)
* stages/user_write: run set_expiry before login, so that session used in Signal has correct expiry

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

* add tests

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-13 15:31:06 +01:00
61bf73d2f9 web/elements: fix copy on insecure origins (#4917)
* web/elements: fix copy on insecure origins

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

* fallback to messages for other clipboard uses

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-13 14:18:48 +01:00
9219abf84b web/admin: fix scim provider layout (#4919)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-13 14:18:35 +01:00
178bfe1d44 providers/scim: handle ServiceProviderConfig 404 (#4915)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-13 13:44:29 +01:00
afb7f8be3e core: bump paramiko from 3.0.0 to 3.1.0 (#4913)
Bumps [paramiko](https://github.com/paramiko/paramiko) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/paramiko/paramiko/releases)
- [Changelog](https://github.com/paramiko/paramiko/blob/main/NEWS)
- [Commits](https://github.com/paramiko/paramiko/compare/3.0.0...3.1.0)

---
updated-dependencies:
- dependency-name: paramiko
  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-03-13 10:47:16 +01:00
ba08060337 web: bump eslint from 8.35.0 to 8.36.0 in /web (#4912)
Bumps [eslint](https://github.com/eslint/eslint) from 8.35.0 to 8.36.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.35.0...v8.36.0)

---
updated-dependencies:
- dependency-name: eslint
  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-03-13 10:46:58 +01:00
26243c05ed core: bump urllib3 from 1.26.14 to 1.26.15 (#4914)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.14 to 1.26.15.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.14...1.26.15)

---
updated-dependencies:
- dependency-name: urllib3
  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-03-13 10:36:43 +01:00
56375d7245 web/flows: fix compatibility mode (#4910)
* fix compatibility mode

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

* attach stylesheets to document instead of nothing, fix dark theme

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-12 22:19:03 +01:00
94f22cffba root: fix session middleware for websocket connections (#4909)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-12 16:47:19 +01:00
10b7d78825 events: set task start time before start not on init (#4908)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-12 15:13:04 +01:00
7618c2e45f website/docs: improve traefik standalone docs (#4493)
* Create _traefik_standalone_single_application.md 

Example for Authentik Single Application Proxy with Service example because this was unclear for many users and if you dont create a middleware for every application you get the error "no app for hostname". 

Signed-off-by: support-tt <61587422+support-tt@users.noreply.github.com>

* Update _traefik_standalone_single_application.md

Signed-off-by: support-tt <61587422+support-tt@users.noreply.github.com>

* rename to old file

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

---------

Signed-off-by: support-tt <61587422+support-tt@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-03-10 13:45:41 -06:00
e13615c1ae web/elements: fix flipped theme in codemirror (#4901) 2023-03-10 20:11:02 +01:00
06fb81410b website/docs: fix layout for preview annotation (#4899)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-10 17:38:32 +01:00
5732fc0c2e website/docs: prepare 2023.3 release notes (#4889)
* website/docs: prepare 2023.3 release notes

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

* Apply suggestions from code review

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

* add better docs for custom css

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-03-10 17:33:59 +01:00
59e54901fb web: fix theming issues when using automatic (#4898)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-10 17:33:03 +01:00
0ef333f8ea core: bump bandit from 1.7.4 to 1.7.5 (#4896)
* core: bump bandit from 1.7.4 to 1.7.5

Bumps [bandit](https://github.com/PyCQA/bandit) from 1.7.4 to 1.7.5.
- [Release notes](https://github.com/PyCQA/bandit/releases)
- [Commits](https://github.com/PyCQA/bandit/compare/1.7.4...1.7.5)

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

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

* fix

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-03-10 12:06:59 +01:00
12ef7e2fae web: bump turnstile-types from 1.1.1 to 1.1.2 in /web (#4892)
Bumps [turnstile-types](https://github.com/le0developer/turnstile-types) from 1.1.1 to 1.1.2.
- [Release notes](https://github.com/le0developer/turnstile-types/releases)
- [Changelog](https://github.com/Le0Developer/turnstile-types/blob/master/HISTORY.md)
- [Commits](https://github.com/le0developer/turnstile-types/compare/v1.1.1...v1.1.2)

---
updated-dependencies:
- dependency-name: turnstile-types
  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-03-10 11:56:45 +01:00
397d6ff059 web: bump @sentry/tracing from 7.41.0 to 7.42.0 in /web (#4894)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 7.41.0 to 7.42.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.41.0...7.42.0)

---
updated-dependencies:
- dependency-name: "@sentry/tracing"
  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-03-10 11:56:35 +01:00
6469698261 core: bump goauthentik.io/api/v3 from 3.2023022.14 to 3.2023022.15 (#4893)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023022.14 to 3.2023022.15.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023022.14...v3.2023022.15)

---
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-03-10 11:56:18 +01:00
3b733e98fa web: bump @sentry/browser from 7.41.0 to 7.42.0 in /web (#4891)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 7.41.0 to 7.42.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.41.0...7.42.0)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  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-03-10 11:49:00 +01:00
9e855d1f0e core: bump twilio from 7.16.4 to 7.16.5 (#4895)
Bumps [twilio](https://github.com/twilio/twilio-python) from 7.16.4 to 7.16.5.
- [Release notes](https://github.com/twilio/twilio-python/releases)
- [Changelog](https://github.com/twilio/twilio-python/blob/main/CHANGES.md)
- [Commits](https://github.com/twilio/twilio-python/compare/7.16.4...7.16.5)

---
updated-dependencies:
- dependency-name: twilio
  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-03-10 11:48:49 +01:00
a2ee76b328 core: bump uvicorn from 0.20.0 to 0.21.0 (#4897)
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.20.0 to 0.21.0.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.20.0...0.21.0)

---
updated-dependencies:
- dependency-name: uvicorn
  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-03-10 11:39:45 +01:00
86bb2afd02 core: add validator which allows for URLs with formatting (#4890) 2023-03-10 00:16:17 +01:00
9b8c0e3924 web: fix locale inconsistencies (#4888)
start fixing locale inconsistencies

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-09 23:57:54 +01:00
d11ee46589 web: bump API Client version (#4887) 2023-03-09 23:22:05 +01:00
b6b820f6f1 web: toggle dark/light theme manually (#4876) 2023-03-09 23:17:53 +01:00
e28f897cb1 core: bump pylint from 2.16.4 to 2.17.0 (#4884)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.16.4 to 2.17.0.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.16.4...v2.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-09 12:21:50 +01:00
964f095630 core: bump goauthentik.io/api/v3 from 3.2023022.12 to 3.2023022.14 (#4883)
Bumps goauthentik.io/api/v3 from 3.2023022.12 to 3.2023022.14.

---
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-03-09 12:21:26 +01:00
610012fcc3 core: bump codespell from 2.2.2 to 2.2.4 (#4881)
Bumps [codespell](https://github.com/codespell-project/codespell) from 2.2.2 to 2.2.4.
- [Release notes](https://github.com/codespell-project/codespell/releases)
- [Commits](https://github.com/codespell-project/codespell/compare/v2.2.2...v2.2.4)

---
updated-dependencies:
- dependency-name: codespell
  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-03-09 12:20:59 +01:00
3c970a135c web: bump pyright from 1.1.297 to 1.1.298 in /web (#4882)
Bumps [pyright](https://github.com/Microsoft/pyright/tree/HEAD/packages/pyright) from 1.1.297 to 1.1.298.
- [Release notes](https://github.com/Microsoft/pyright/releases)
- [Commits](https://github.com/Microsoft/pyright/commits/1.1.298/packages/pyright)

---
updated-dependencies:
- dependency-name: pyright
  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-03-09 12:19:58 +01:00
24d7cebbe7 core: bump golang from 1.20.1-bullseye to 1.20.2-bullseye (#4871)
Bumps golang from 1.20.1-bullseye to 1.20.2-bullseye.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-08 10:52:18 +01:00
618527b51c web: bump pyright from 1.1.296 to 1.1.297 in /web (#4872)
Bumps [pyright](https://github.com/Microsoft/pyright/tree/HEAD/packages/pyright) from 1.1.296 to 1.1.297.
- [Release notes](https://github.com/Microsoft/pyright/releases)
- [Commits](https://github.com/Microsoft/pyright/commits/1.1.297/packages/pyright)

---
updated-dependencies:
- dependency-name: pyright
  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-03-08 10:51:50 +01:00
4afcc240a3 core: bump goauthentik.io/api/v3 from 3.2023022.11 to 3.2023022.12 (#4874)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023022.11 to 3.2023022.12.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023022.11...v3.2023022.12)

---
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-03-08 10:51:41 +01:00
26308ef62b core: bump django-otp from 1.1.5 to 1.1.6 (#4873)
Bumps [django-otp](https://github.com/django-otp/django-otp) from 1.1.5 to 1.1.6.
- [Release notes](https://github.com/django-otp/django-otp/releases)
- [Changelog](https://github.com/django-otp/django-otp/blob/master/CHANGES.rst)
- [Commits](https://github.com/django-otp/django-otp/compare/v1.1.5...v1.1.6)

---
updated-dependencies:
- dependency-name: django-otp
  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-03-08 10:51:05 +01:00
36ed62142d core: add a list of recommended vs code extensions (#4869)
add a list of recommended vs code extensions

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-08 00:25:59 +01:00
6ae2fc9668 providers/SCIM: customizable externalId, document behavior (#4868)
* only set externalId if mapping hasn't set it

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

* better document use of SCIM in conjunction with OAuth/SAML

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-08 00:15:16 +01:00
34f01d3731 website/docs: fix typo (#4867)
Update index.mdx

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
2023-03-07 23:53:05 +01:00
67f3db1e03 core: enforce unique on names where it makes sense (#4866)
enforce unique on names where it makes sense

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-07 23:52:34 +01:00
36f92f01de website/blog: Becoming OpenID certified - Why standards matter (#4865)
* website/blog: Becoming OpenID certified - Why standards matter

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

* Apply suggestions from code review

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

* Update website/blog/2023-03-07-becoming-openid-certified-why-standards-matter/index.md

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-03-07 18:22:53 +01:00
f19c143e95 web: bump API Client version (#4864)
Signed-off-by: GitHub <noreply@github.com>
2023-03-07 15:44:45 +01:00
9559bc2e1e providers/scim: add option to filter out service accounts, parent group (#4862)
* add option to filter out service accounts, parent group

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

* update docs

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

* rename to filter group

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

* rework sync card to show scim sync status

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-07 15:39:48 +01:00
41d17dc543 internal: fix crash when port 9000 is in use (#4863)
fix crash when port 9000 is in use

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-07 13:27:46 +01:00
885aeddbdc web: bump @sentry/browser from 7.40.0 to 7.41.0 in /web (#4855)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 7.40.0 to 7.41.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.40.0...7.41.0)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  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-03-07 10:50:35 +01:00
033e315035 web: bump @typescript-eslint/eslint-plugin from 5.54.0 to 5.54.1 in /web (#4857)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.54.0 to 5.54.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.54.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  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-03-07 10:50:23 +01:00
4539954173 core: bump github.com/getsentry/sentry-go from 0.18.0 to 0.19.0 (#4858)
Bumps [github.com/getsentry/sentry-go](https://github.com/getsentry/sentry-go) from 0.18.0 to 0.19.0.
- [Release notes](https://github.com/getsentry/sentry-go/releases)
- [Changelog](https://github.com/getsentry/sentry-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-go/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: github.com/getsentry/sentry-go
  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-03-07 10:46:14 +01:00
f54351fd57 core: bump goauthentik.io/api/v3 from 3.2023022.10 to 3.2023022.11 (#4859)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023022.10 to 3.2023022.11.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023022.10...v3.2023022.11)

---
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-03-07 10:46:03 +01:00
b61655fe4f web: bump @sentry/tracing from 7.40.0 to 7.41.0 in /web (#4856)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 7.40.0 to 7.41.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.40.0...7.41.0)

---
updated-dependencies:
- dependency-name: "@sentry/tracing"
  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-03-07 10:45:32 +01:00
2a74f5e91f web: bump @typescript-eslint/parser from 5.54.0 to 5.54.1 in /web (#4854)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.54.0 to 5.54.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.54.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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-03-07 10:45:20 +01:00
7ea3fd6482 core: bump pylint from 2.16.3 to 2.16.4 (#4860)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.16.3 to 2.16.4.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.16.3...v2.16.4)

---
updated-dependencies:
- dependency-name: pylint
  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-03-07 10:45:01 +01:00
c02e2c22ff core: bump django-otp from 1.1.4 to 1.1.5 (#4861)
Bumps [django-otp](https://github.com/django-otp/django-otp) from 1.1.4 to 1.1.5.
- [Release notes](https://github.com/django-otp/django-otp/releases)
- [Changelog](https://github.com/django-otp/django-otp/blob/master/CHANGES.rst)
- [Commits](https://github.com/django-otp/django-otp/compare/v1.1.4...v1.1.5)

---
updated-dependencies:
- dependency-name: django-otp
  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-03-07 10:43:31 +01:00
d834ec4db9 web/elements: fix center text not scrolling with container (#4853)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-06 23:21:16 +01:00
f6a8b3d568 website/docs: Corrected typo and added Note about port number if using Istio/Kubern… (#4851)
* Corrected typo and added Note about port number if using Istio/Kubernetes

@BeryJu I was reading [this article](https://prevue.ch/news/2022-10-11-istio-authentik/) about a fellow setting up authentik, using Istio and Kubernetes. I wanted to somehow add a heads up about the port number, but I am not confident that I got it right. Is it only if there are custom decisions being made that the port number has to be for the cluster? 

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

* Update website/docs/providers/proxy/forward_auth.mdx

Signed-off-by: Jens L. <jens@beryju.org>

* fix lint error

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

---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens L. <jens@beryju.org>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens.langhammer@beryju.org>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-03-06 19:28:40 +00:00
c4a7648ce3 website: add website development setup, update contribution guidelines on PR titles (#4852)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-06 19:27:05 +00:00
c0144c9bc1 web: bump API Client version (#4850)
Signed-off-by: GitHub <noreply@github.com>
2023-03-06 19:45:05 +01:00
28ddeb124f providers: SCIM (#4835)
* basic user sync

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

* add group sync and some refactor

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

* start API

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

* allow null authorization flow

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

* add UI

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

* make task monitored

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

* add missing dependency

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

* make authorization_flow required for most providers via API

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

* more UI

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

* make task result better readable, exclude anonymous user

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

* add task UI

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

* add scheduled task for all sync

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

* make scim errors more readable

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

* add mappings, migrate to mappings

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

* add mapping UI and more

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

* add scim docs to web

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

* start implementing membership

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

* migrate signals to tasks

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

* migrate fully to tasks

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

* strip none keys, fix lint errors

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

* fix things

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

* start adding tests

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

* fix saml

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

* add scim schemas and validate against it

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

* improve error handling

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

* add group put support, add group tests

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

* send correct application/scim+json headers

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

* stop sync if no mappings are confiugred

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

* add test for task sync

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

* add membership tests

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

* use decorator for tests

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

* make tests better

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-06 19:39:08 +01:00
dbc07f55f4 core: bump goauthentik.io/api/v3 from 3.2023022.8 to 3.2023022.10 (#4847)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023022.8 to 3.2023022.10.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023022.8...v3.2023022.10)

---
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-03-06 10:53:57 +01:00
0d7c2d8269 core: bump pylint from 2.16.2 to 2.16.3 (#4845)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.16.2 to 2.16.3.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.16.2...v2.16.3)

---
updated-dependencies:
- dependency-name: pylint
  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-03-06 10:53:48 +01:00
879ea8ed62 core: bump pytest from 7.2.1 to 7.2.2 (#4846)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.1 to 7.2.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.2.1...7.2.2)

---
updated-dependencies:
- dependency-name: pytest
  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-03-06 10:53:39 +01:00
54c76735e2 core: bump drf-spectacular from 0.25.1 to 0.26.0 (#4844)
Bumps [drf-spectacular](https://github.com/tfranzel/drf-spectacular) from 0.25.1 to 0.26.0.
- [Release notes](https://github.com/tfranzel/drf-spectacular/releases)
- [Changelog](https://github.com/tfranzel/drf-spectacular/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tfranzel/drf-spectacular/compare/0.25.1...0.26.0)

---
updated-dependencies:
- dependency-name: drf-spectacular
  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-03-06 10:53:26 +01:00
b6f5fed121 core: bump golang.org/x/oauth2 from 0.5.0 to 0.6.0 (#4848)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/golang/oauth2/releases)
- [Commits](https://github.com/golang/oauth2/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  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-03-06 10:52:48 +01:00
e08536af33 web: bump mermaid from 10.0.1 to 10.0.2 in /web (#4837)
* web: bump mermaid from 10.0.1 to 10.0.2 in /web

Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 10.0.1 to 10.0.2.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/mermaid-js/mermaid/compare/v10.0.1...v10.0.2)

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

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

* fix failing bandit check

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-03-03 10:27:16 +01:00
2c32e54746 website: bump dns-packet from 5.3.1 to 5.4.0 in /website (#4836) 2023-03-03 00:05:22 +01:00
9370d155f8 sources/plex: fix check_token error unusable if token is empty (#4834)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-02 22:21:54 +00:00
e47bbe63b8 website/docs: update release notes (#4833)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-02 20:27:51 +01:00
972dce1462 security: fix CVE-2023-26481 (#4832)
fix CVE-2023-26481

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-02 20:15:33 +01:00
7b44d8972f stages/authenticator_sms: fix twilio sending, add test (#4829)
closes #4823

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-02 14:39:28 +01:00
ba9cafecc5 web: bump @sentry/browser from 7.39.0 to 7.40.0 in /web (#4826)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 7.39.0 to 7.40.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.40.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.39.0...7.40.0)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  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-03-02 11:57:01 +01:00
e0f4f2c80a web: bump mermaid from 10.0.0 to 10.0.1 in /web (#4825)
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 10.0.0 to 10.0.1.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/mermaid-js/mermaid/compare/v10.0.0...v10.0.1)

---
updated-dependencies:
- dependency-name: mermaid
  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-03-02 11:56:13 +01:00
e715f1fbbb web: bump @sentry/tracing from 7.39.0 to 7.40.0 in /web (#4827)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 7.39.0 to 7.40.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.40.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.39.0...7.40.0)

---
updated-dependencies:
- dependency-name: "@sentry/tracing"
  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-03-02 11:55:45 +01:00
dde9d02008 web: bump @codemirror/lang-python from 6.1.1 to 6.1.2 in /web (#4828)
Bumps [@codemirror/lang-python](https://github.com/codemirror/lang-python) from 6.1.1 to 6.1.2.
- [Release notes](https://github.com/codemirror/lang-python/releases)
- [Changelog](https://github.com/codemirror/lang-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/lang-python/compare/6.1.1...6.1.2)

---
updated-dependencies:
- dependency-name: "@codemirror/lang-python"
  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-03-02 11:55:29 +01:00
a6eba37d5a core: Add resolve_dns and reverse_dns functions to evaluator (#4769)
* Add resolve_dns

* Add reverse_dns

* Fix lint

* add caching, small optimisation

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

* Added time-aware LRU cache

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-03-01 22:15:13 +01:00
2eb7c16a9a web/admin: set valid correctly when opened and radio is already selected (#4821)
closes #4813

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-01 16:14:32 +01:00
87fa50c492 web/admin: workaround for tenant certificate selection being cut off (#4820)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

#4814
2023-03-01 15:41:28 +01:00
9042664fcf web: bump pyright from 1.1.295 to 1.1.296 in /web (#4818)
Bumps [pyright](https://github.com/Microsoft/pyright/tree/HEAD/packages/pyright) from 1.1.295 to 1.1.296.
- [Release notes](https://github.com/Microsoft/pyright/releases)
- [Commits](https://github.com/Microsoft/pyright/commits/1.1.296/packages/pyright)

---
updated-dependencies:
- dependency-name: pyright
  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-03-01 11:02:14 +01:00
e8a53041cc core: bump watchdog from 2.3.0 to 2.3.1 (#4819)
Bumps [watchdog](https://github.com/gorakhargosh/watchdog) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/gorakhargosh/watchdog/releases)
- [Changelog](https://github.com/gorakhargosh/watchdog/blob/master/changelog.rst)
- [Commits](https://github.com/gorakhargosh/watchdog/compare/v2.3.0...v2.3.1)

---
updated-dependencies:
- dependency-name: watchdog
  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-03-01 10:59:42 +01:00
20e971f5ce flows: planner error handling (#4812)
* handle FlowNonApplicableException everywhere

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

* make flow planner check authentication when no pending user is in planning context

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

* add mailhog to e2e test services, remove local docker requirement

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-28 15:18:29 +01:00
6f2f4f4aa3 web: bump @sentry/browser from 7.38.0 to 7.39.0 in /web (#4807)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 7.38.0 to 7.39.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.38.0...7.39.0)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  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-02-28 11:11:38 +01:00
66e8748503 web: bump @sentry/tracing from 7.38.0 to 7.39.0 in /web (#4806)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 7.38.0 to 7.39.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.38.0...7.39.0)

---
updated-dependencies:
- dependency-name: "@sentry/tracing"
  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-02-28 10:55:42 +01:00
b5c8fa24a2 web: bump @typescript-eslint/parser from 5.53.0 to 5.54.0 in /web (#4810)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.53.0 to 5.54.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.54.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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-02-28 10:55:10 +01:00
335e124c0a core: bump goauthentik.io/api/v3 from 3.2023022.6 to 3.2023022.8 (#4811)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023022.6 to 3.2023022.8.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023022.6...v3.2023022.8)

---
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-02-28 10:54:31 +01:00
1faf3c66c7 core: bump sentry-sdk from 1.15.0 to 1.16.0 (#4809)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 1.15.0 to 1.16.0.
- [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.15.0...1.16.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  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-02-28 10:53:22 +01:00
7810063ca0 web: bump @typescript-eslint/eslint-plugin from 5.53.0 to 5.54.0 in /web (#4808)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.53.0 to 5.54.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.54.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  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-02-28 10:53:02 +01:00
980320e24b tests/e2e: use example blueprints for testing (#4805)
* tests/e2e: use blueprints for testing

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

* add identification stage assignment

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

* add recovery flow tests

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-27 22:42:36 +01:00
118765ab30 web: fetch custom.css via fetch and add stylesheet (#4804)
* web: fetch custom.css via fetch and add stylesheet

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

* don't hardcode path

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-27 19:54:19 +01:00
5e60db8593 providers/oauth2: fix typo (#4803) 2023-02-27 17:17:48 +01:00
e81a065855 web: bump API Client version (#4801)
Signed-off-by: GitHub <noreply@github.com>
2023-02-27 15:26:11 +01:00
39d0893303 flows: change default flow stage binding settings (#4784)
* flows: change default flow stage binding settings

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

* fallback to correct value

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-27 15:21:26 +01:00
99ddbf553c website: add X-Frame-Options (#4800)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-27 13:40:33 +01:00
799b509ac6 web: bump @lingui/cli from 3.17.1 to 3.17.2 in /web (#4792)
Bumps [@lingui/cli](https://github.com/lingui/js-lingui) from 3.17.1 to 3.17.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/v3.17.1...v3.17.2)

---
updated-dependencies:
- dependency-name: "@lingui/cli"
  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-02-27 10:54:28 +01:00
bc6b591dfb web: bump @lingui/macro from 3.17.1 to 3.17.2 in /web (#4797)
Bumps [@lingui/macro](https://github.com/lingui/js-lingui) from 3.17.1 to 3.17.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/v3.17.1...v3.17.2)

---
updated-dependencies:
- dependency-name: "@lingui/macro"
  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-02-27 10:52:47 +01:00
ef6a799533 web: bump @lingui/core from 3.17.1 to 3.17.2 in /web (#4791)
Bumps [@lingui/core](https://github.com/lingui/js-lingui) from 3.17.1 to 3.17.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/v3.17.1...v3.17.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-27 10:52:03 +01:00
26de143bf8 core: bump github.com/stretchr/testify from 1.8.1 to 1.8.2 (#4799)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.1 to 1.8.2.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.1...v1.8.2)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  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-02-27 10:44:35 +01:00
ad46b3f05c core: bump coverage from 7.2.0 to 7.2.1 (#4794)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.2.0 to 7.2.1.
- [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.0...7.2.1)

---
updated-dependencies:
- dependency-name: coverage
  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-02-27 10:44:16 +01:00
0462afe964 web: bump core-js from 3.28.0 to 3.29.0 in /web (#4796)
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.28.0 to 3.29.0.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.29.0/packages/core-js)

---
updated-dependencies:
- dependency-name: core-js
  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-02-27 10:44:04 +01:00
1100b98596 web: bump @lingui/detect-locale from 3.17.1 to 3.17.2 in /web (#4793)
Bumps [@lingui/detect-locale](https://github.com/lingui/js-lingui) from 3.17.1 to 3.17.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/v3.17.1...v3.17.2)

---
updated-dependencies:
- dependency-name: "@lingui/detect-locale"
  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-02-27 10:43:50 +01:00
89ccdfcf6e web: bump @trivago/prettier-plugin-sort-imports from 4.1.0 to 4.1.1 in /web (#4790)
web: bump @trivago/prettier-plugin-sort-imports in /web

Bumps [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases)
- [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/master/CHANGELOG.md)
- [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: "@trivago/prettier-plugin-sort-imports"
  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-02-27 10:43:41 +01:00
a328d2d68a web: bump eslint from 8.34.0 to 8.35.0 in /web (#4795)
Bumps [eslint](https://github.com/eslint/eslint) from 8.34.0 to 8.35.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.34.0...v8.35.0)

---
updated-dependencies:
- dependency-name: eslint
  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-02-27 10:43:30 +01:00
d5a94ea687 core: bump goauthentik.io/api/v3 from 3.2023022.5 to 3.2023022.6 (#4798)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023022.5 to 3.2023022.6.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023022.5...v3.2023022.6)

---
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-02-27 10:40:05 +01:00
596ff529c4 core: bootstrap email (#4788) 2023-02-26 17:02:45 +01:00
612d1c76d4 web/admin: fix chart display with no sources (#4782) 2023-02-24 22:54:11 +01:00
886749dcb2 web: bump @braintree/sanitize-url from 6.0.0 to 6.0.2 in /web (#4781)
Bumps [@braintree/sanitize-url](https://github.com/braintree/sanitize-url) from 6.0.0 to 6.0.2.
- [Release notes](https://github.com/braintree/sanitize-url/releases)
- [Changelog](https://github.com/braintree/sanitize-url/blob/main/CHANGELOG.md)
- [Commits](https://github.com/braintree/sanitize-url/compare/v6.0.0...v6.0.2)

---
updated-dependencies:
- dependency-name: "@braintree/sanitize-url"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-24 21:16:26 +01:00
26f3275361 sources/ldap: improve error handling for password complexity (#4780)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-24 10:39:43 +00:00
6441401d94 web: bump API Client version (#4779)
Signed-off-by: GitHub <noreply@github.com>
2023-02-24 11:24:51 +01:00
b7e4ad7234 web/user: fix source connections not being filtered (#4778)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-24 10:22:02 +00:00
9c82024fd5 core: bump golang.org/x/sync from 0.0.0-20220601150217-0de741cfad7f to 0.1.0 (#4774)
core: bump golang.org/x/sync

Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.0.0-20220601150217-0de741cfad7f to 0.1.0.
- [Release notes](https://github.com/golang/sync/releases)
- [Commits](https://github.com/golang/sync/commits/v0.1.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  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-02-24 10:47:05 +01:00
685709decb core: bump goauthentik.io/api/v3 from 3.2023022.4 to 3.2023022.5 (#4773)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023022.4 to 3.2023022.5.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023022.4...v3.2023022.5)

---
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-02-24 10:46:36 +01:00
6ab2afc8d4 web: bump @trivago/prettier-plugin-sort-imports from 4.0.0 to 4.1.0 in /web (#4771)
web: bump @trivago/prettier-plugin-sort-imports in /web

Bumps [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases)
- [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/master/CHANGELOG.md)
- [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v4.0.0...v4.1.0)

---
updated-dependencies:
- dependency-name: "@trivago/prettier-plugin-sort-imports"
  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-02-24 10:45:09 +01:00
23ad132f74 core: bump watchdog from 2.2.1 to 2.3.0 (#4772) 2023-02-24 10:31:40 +01:00
87164f5cdb core: bump golang.org/x/oauth2 from 0.0.0-20220223155221-ee480838109b to 0.5.0 (#4775) 2023-02-24 10:31:08 +01:00
d6056755b3 web: give node more memory to build (#4768)
it seems to sometimes fail with an OOM message
2023-02-23 20:45:48 +01:00
36229f4224 blueprints: improve error handling in example flow
closes #4714

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-23 16:57:46 +01:00
80f4fccd35 providers/oauth2: OpenID conformance (#4758)
* don't open inspector by default when debug is enabled

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

* encode error in fragment when using hybrid grant_type

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

* require nonce for all response_types that get an id_token from the authorization endpoint

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

* don't set empty family_name

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

* only set at_hash when response has token

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

* cleaner way to get login time

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

* remove authentication requirement from authentication flow

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

* use wrapper

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

* fix auth_time not being handled correctly

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

* minor cleanup

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

* add test files

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

* fix tests

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

* remove USER_LOGIN_AUTHENTICATED

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

* rework prompt=login handling

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

* also set last login uid for max_age check to prevent double login when max_age and prompt=login is set

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-23 15:26:41 +01:00
c6a14fa4f1 web: bump @goauthentik/api from 2023.2.2-1677071401 to 2023.2.2-1677073316 in /web (#4761) 2023-02-23 10:36:04 +01:00
c6235e0f1e core: bump goauthentik.io/api/v3 from 3.2023022.2 to 3.2023022.4 (#4762) 2023-02-23 10:35:33 +01:00
7c946c1cbe web: bump pyright from 1.1.294 to 1.1.295 in /web (#4760) 2023-02-23 10:35:22 +01:00
664d8646bb core: bump twilio from 7.16.3 to 7.16.4 (#4763) 2023-02-23 10:34:28 +01:00
1aba27c84f core: bump coverage from 7.1.0 to 7.2.0 (#4764) 2023-02-23 10:34:11 +01:00
008729700d core: bump golang.org/x/text from 0.3.7 to 0.3.8 (#4765) 2023-02-23 10:33:56 +01:00
9e1cedbece providers/ldap: fix tests (#4759) 2023-02-23 00:55:43 +01:00
7503b32c74 website/integrations: Zammad instructions (#4644)
* add zammad

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* some improvements

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* add navi-item

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* fix mappings

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* typo

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* personalized link removed

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* replace inventory placeholder & fix SAML

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* Replace placeholder

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* text improvement

Signed-off-by: Tealk <tealk@rollenspiel.monster>

---------

Signed-off-by: Tealk <tealk@rollenspiel.monster>
2023-02-22 16:55:32 +00:00
383b6a38ba website/integrations: Mastodon integration (#4733)
* init mastodon integration

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* replace inventory placeholder

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* Replace placeholder

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* replace username with sub

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* text improvement

Signed-off-by: Tealk <tealk@rollenspiel.monster>

---------

Signed-off-by: Tealk <tealk@rollenspiel.monster>
2023-02-22 17:23:38 +01:00
7d9eef37ed website/integrations: Mobilizon instructions (#4747)
* add mobilizonintegration

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* replace inventory placeholder

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* Replace placeholder

Signed-off-by: Tealk <tealk@rollenspiel.monster>

* text improvement

Signed-off-by: Tealk <tealk@rollenspiel.monster>

---------

Signed-off-by: Tealk <tealk@rollenspiel.monster>
2023-02-22 16:20:47 +00:00
60d3da20f3 website/integrations: fix Vikunja setup instructions (#4730)
* fix: Vikunja setup instructions

Signed-off-by: kolaente <k@knt.li>

* fix: clarify what needs restarting after config change

---------

Signed-off-by: kolaente <k@knt.li>
2023-02-22 15:31:18 +00:00
cd99b6e48f providers/ldap: making ldap compatible with synology (#4694)
* internal/outpost/ldap: making ldap compatible with synology

* fix duplicate attributes

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

* add docs about homedirectory

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

* fix duplicate attributes

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

* add substitution to values

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-02-22 15:26:41 +01:00
51c6a14786 providers/ldap: Improve compatibility with LDAP clients (#4750)
* Fixed invalid LDAP attributes by replacing '.'s and '/'s with '-'

* Leave old fields for now for backward compatibility

* Add forgotten depreceated field

* Fix tests

* Fix tests

* use shorter attribute names

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

* sanitize attributes

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

* keep both sanitized and unsanitized user fields

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

* add sanitized fields to test

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-02-22 14:18:22 +01:00
75866406dc web: bump API Client version (#4757)
Signed-off-by: GitHub <noreply@github.com>
2023-02-22 14:13:16 +01:00
122055b38b stages/user_login: terminate others (#4754)
* rework session list

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

* use sender filtering for signals when possible

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

* add terminate_other_sessions

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-22 14:09:28 +01:00
e68e6cb666 web: bump API Client version (#4756)
Signed-off-by: GitHub <noreply@github.com>
2023-02-22 13:24:21 +01:00
b61d181ec7 website/docs: add better explanation for goauthentik.io/user/token-ex… (#4755)
website/docs: add better explanation for goauthentik.io/user/token-expires

closes #4727
2023-02-22 13:24:04 +01:00
c4e24c04f6 core: Improve service account creation (#4751)
* Added ability to select service account token expiration on creation

* Added call to user.set_unusable_password on service account creation

* Added forgotten call to save()

* Added and improved existsing tests

* Added accidentally deleted help text

* Fix lint
2023-02-22 13:19:01 +01:00
47e663f48c web: bump mermaid from 9.4.0 to 10.0.0 in /web (#4752)
* web: bump mermaid from 9.4.0 to 10.0.0 in /web

Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 9.4.0 to 10.0.0.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/mermaid-js/mermaid/compare/v9.4.0...v10.0.0)

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

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

* update diagram element for mermaid v10

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-02-22 11:33:08 +01:00
1f7178c3a8 providers/oauth2: remove unused import
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-22 11:11:20 +01:00
cfa2edebcf providers/oauth2: revert PKCE requirement for public clients
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-21 23:51:27 +01:00
175502b053 core: Fix bug causing whitespace only names to raise exception when generating avatars (#4746)
Fix bug causing whitespace only names to raise exception when generating avatars

Signed-off-by: sdimovv <36302090+sdimovv@users.noreply.github.com>
2023-02-21 16:19:19 +01:00
2c78053631 website/docs: add release note titles
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-21 12:27:24 +01:00
53c03f3635 web/admin: fix mismatched values in charts
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-21 11:59:33 +01:00
6c72c97513 web: bump @babel/plugin-proposal-decorators from 7.20.13 to 7.21.0 in /web (#4742)
web: bump @babel/plugin-proposal-decorators in /web

Bumps [@babel/plugin-proposal-decorators](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-proposal-decorators) from 7.20.13 to 7.21.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.21.0/packages/babel-plugin-proposal-decorators)

---
updated-dependencies:
- dependency-name: "@babel/plugin-proposal-decorators"
  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-02-21 11:03:02 +01:00
5748b29c03 web: bump @typescript-eslint/parser from 5.52.0 to 5.53.0 in /web (#4739)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.52.0 to 5.53.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.53.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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-02-21 11:01:50 +01:00
87ee4635b2 web: bump @babel/core from 7.20.12 to 7.21.0 in /web (#4740)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.20.12 to 7.21.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.21.0/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  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-02-21 11:01:24 +01:00
9e82de33e6 lib: remove unused imports
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-21 11:00:54 +01:00
8829f76183 web: bump @formatjs/intl-listformat from 7.1.8 to 7.1.9 in /web (#4741)
Bumps [@formatjs/intl-listformat](https://github.com/formatjs/formatjs) from 7.1.8 to 7.1.9.
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/@formatjs/intl-listformat@7.1.8...@formatjs/intl-listformat@7.1.9)

---
updated-dependencies:
- dependency-name: "@formatjs/intl-listformat"
  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-02-21 11:00:36 +01:00
f6165bac8f core: bump goauthentik.io/api/v3 from 3.2023022.1 to 3.2023022.2 (#4738)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023022.1 to 3.2023022.2.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023022.1...v3.2023022.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-02-21 11:00:23 +01:00
84bd6131a1 web: bump @babel/plugin-transform-runtime from 7.19.6 to 7.21.0 in /web (#4737)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.19.6 to 7.21.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.21.0/packages/babel-plugin-transform-runtime)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-runtime"
  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-02-21 11:00:09 +01:00
6d1de4bbd9 web: bump @babel/preset-typescript from 7.18.6 to 7.21.0 in /web (#4736)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.18.6 to 7.21.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.21.0/packages/babel-preset-typescript)

---
updated-dependencies:
- dependency-name: "@babel/preset-typescript"
  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-02-21 10:59:58 +01:00
5a8fbc2f95 web: bump @typescript-eslint/eslint-plugin from 5.52.0 to 5.53.0 in /web (#4743)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.52.0 to 5.53.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.53.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  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-02-21 10:58:59 +01:00
d2cfb76a7c root: don't trace websockets to sentry
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-20 21:32:35 +01:00
f70be86ddc providers/proxy: strip scheme when comparing redirect URL
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-20 21:22:26 +01:00
f5eb414d14 web: bump API Client version (#4728)
Signed-off-by: GitHub <noreply@github.com>
2023-02-20 12:51:02 +01:00
327d87355d lib: improve caching of gravatar status
closes #4711

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-20 12:41:09 +01:00
b415e9b773 core: remove avatar from group user member list
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

#4711
2023-02-20 12:40:42 +01:00
b203de7a26 web: bump @sentry/tracing from 7.37.2 to 7.38.0 in /web (#4721)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 7.37.2 to 7.38.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.37.2...7.38.0)

---
updated-dependencies:
- dependency-name: "@sentry/tracing"
  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-02-20 11:24:33 +01:00
ee65877956 web: bump @formatjs/intl-listformat from 7.1.7 to 7.1.8 in /web (#4720)
Bumps [@formatjs/intl-listformat](https://github.com/formatjs/formatjs) from 7.1.7 to 7.1.8.
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/@formatjs/intl-listformat@7.1.7...@formatjs/intl-listformat@7.1.8)

---
updated-dependencies:
- dependency-name: "@formatjs/intl-listformat"
  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-02-20 11:24:21 +01:00
c5097bfc5a web: bump @codemirror/theme-one-dark from 6.1.0 to 6.1.1 in /web (#4722)
Bumps [@codemirror/theme-one-dark](https://github.com/codemirror/theme-one-dark) from 6.1.0 to 6.1.1.
- [Release notes](https://github.com/codemirror/theme-one-dark/releases)
- [Changelog](https://github.com/codemirror/theme-one-dark/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/theme-one-dark/compare/6.1.0...6.1.1)

---
updated-dependencies:
- dependency-name: "@codemirror/theme-one-dark"
  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-02-20 11:11:21 +01:00
febb6f57bd web: bump @sentry/browser from 7.37.2 to 7.38.0 in /web (#4724)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 7.37.2 to 7.38.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.37.2...7.38.0)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  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-02-20 11:11:07 +01:00
843cbd4674 core: bump webauthn from 1.7.0 to 1.7.2 (#4723)
Bumps [webauthn](https://github.com/duo-labs/py_webauthn) from 1.7.0 to 1.7.2.
- [Release notes](https://github.com/duo-labs/py_webauthn/releases)
- [Changelog](https://github.com/duo-labs/py_webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/duo-labs/py_webauthn/compare/v1.7.0...v1.7.2)

---
updated-dependencies:
- dependency-name: webauthn
  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-02-20 11:10:54 +01:00
a4a82bd041 core: bump selenium from 4.8.0 to 4.8.2 (#4725)
Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.8.0 to 4.8.2.
- [Release notes](https://github.com/SeleniumHQ/Selenium/releases)
- [Commits](https://github.com/SeleniumHQ/Selenium/commits)

---
updated-dependencies:
- dependency-name: selenium
  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-02-20 11:10:39 +01:00
ca2a59281a core: bump duo-client from 4.5.0 to 4.6.1 (#4726)
Bumps [duo-client](https://github.com/duosecurity/duo_client_python) from 4.5.0 to 4.6.1.
- [Release notes](https://github.com/duosecurity/duo_client_python/releases)
- [Commits](https://github.com/duosecurity/duo_client_python/compare/4.5.0...4.6.1)

---
updated-dependencies:
- dependency-name: duo-client
  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-02-20 11:08:47 +01:00
6f1721a728 web: refactor rendering of source icons
closes #4718

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-19 20:17:58 +01:00
99baf1a29e web/elements: add loading spinner for charts, render middle text with css
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-19 20:10:37 +01:00
a68fa06ff9 web/flows: fix fa:// icons in sources not shown correctly
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-19 19:45:57 +01:00
9f431396c0 providers/proxy: ensure issuer is correct when browser url override is set
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

#4715
2023-02-19 17:35:29 +01:00
1ac2e924a2 core: fix error when creating token without request in context
closes #4716

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-19 17:31:20 +01:00
0874574e5c *: add additional prometheus metrics, remove unusable high entropy metrics
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-19 17:08:40 +01:00
069e9c015b events: fix m2m_change events not being logged
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-19 16:28:30 +01:00
8de4471322 website: update blog
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-17 19:35:39 +01:00
c6ead3dc49 providers/oauth2: make PKCE required for public clients
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-17 18:08:39 +01:00
f749027143 root: don't log django request warnings
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-17 18:08:18 +01:00
153bd3aaf1 sources/oauth: fix not all token errors being logged with response
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-17 13:22:41 +01:00
e19c4886fe ci: bump snok/container-retention-policy from 1 to 2 (#4710)
Bumps [snok/container-retention-policy](https://github.com/snok/container-retention-policy) from 1 to 2.
- [Release notes](https://github.com/snok/container-retention-policy/releases)
- [Commits](https://github.com/snok/container-retention-policy/compare/v1...v2)

---
updated-dependencies:
- dependency-name: snok/container-retention-policy
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-17 09:44:04 +01:00
1a57d453ba providers/oauth2: fix missing information for Revoked token access events
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-16 14:47:07 +01:00
e5dfe7dafe website: always show build version in version dropdown
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

#3940
2023-02-16 14:38:58 +01:00
bb190852a5 web: bump pyright from 1.1.293 to 1.1.294 in /web (#4703)
Bumps [pyright](https://github.com/Microsoft/pyright/tree/HEAD/packages/pyright) from 1.1.293 to 1.1.294.
- [Release notes](https://github.com/Microsoft/pyright/releases)
- [Commits](https://github.com/Microsoft/pyright/commits/1.1.294/packages/pyright)

---
updated-dependencies:
- dependency-name: pyright
  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-02-16 11:22:36 +01:00
34a2d105d3 web: bump mermaid from 9.3.0 to 9.4.0 in /web (#4704)
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 9.3.0 to 9.4.0.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/mermaid-js/mermaid/compare/v9.3.0...v9.4.0)

---
updated-dependencies:
- dependency-name: mermaid
  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-02-16 11:22:17 +01:00
80601e16f9 core: bump goauthentik.io/api/v3 from 3.2023021.1 to 3.2023022.1 (#4705)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023021.1 to 3.2023022.1.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023021.1...v3.2023022.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-02-16 11:21:31 +01:00
ff6f9cc44f core: bump kubernetes from 25.3.0 to 26.1.0 (#4706)
Bumps [kubernetes](https://github.com/kubernetes-client/python) from 25.3.0 to 26.1.0.
- [Release notes](https://github.com/kubernetes-client/python/releases)
- [Changelog](https://github.com/kubernetes-client/python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes-client/python/compare/v25.3.0...v26.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-16 11:21:18 +01:00
e0f60e09cf web: bump API Client version (#4700)
Signed-off-by: GitHub <noreply@github.com>
2023-02-15 22:26:22 +01:00
176aa606ca Merge branch 'version-2023.2' 2023-02-15 21:28:28 +01:00
17364c3bd8 website/docs: add 2023.2.2 release notes
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 20:34:25 +01:00
d842fc4958 release: 2023.2.2 2023-02-15 19:53:42 +01:00
19f5e6e07e website/docs: update events page
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 16:44:13 +01:00
acfa9c76d1 providers/ldap: check MFA password on password stage
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 16:27:08 +01:00
bff34cc5dc root: use channel send workaround for sync sending of websocket messages
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 16:08:01 +01:00
7f009f6d02 flows: include flow authentication requirement in diagram
closes #4533

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 16:04:45 +01:00
dfb9ae548c web/admin: fix error when creating new users
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

#4685
2023-02-15 15:32:48 +01:00
7d6b573f8b website: migrate to mermaid charts, rework proxy page
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 12:14:17 +01:00
ade397fc24 web/user: revert truncate behaviour for application description
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 11:17:45 +01:00
d945d30cda providers/proxy: fix value is too long with filesystem sessions
closes #4693

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 10:50:01 +01:00
c8c401e2c5 lib: don't try to cache generated avatar with full user, only cache with name
closes #4690

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 10:49:13 +01:00
e4ca20bfc6 core: bump golang from 1.20.0-bullseye to 1.20.1-bullseye (#4691)
Bumps golang from 1.20.0-bullseye to 1.20.1-bullseye.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-15 10:46:02 +01:00
6347716815 core: bump goauthentik.io/api/v3 from 3.2023012.5 to 3.2023021.1 (#4692)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023012.5 to 3.2023021.1.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023012.5...v3.2023021.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-02-15 10:45:20 +01:00
859b6cc60e website: adjust padding on hero header
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 00:28:45 +01:00
06a1a7f076 ci: add time limits to ci jobs
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 00:28:36 +01:00
b6c120f555 providers/proxy: fix client credential flows not using http interceptor
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 00:22:56 +01:00
6cc363bc5d web: bump API Client version (#4689)
Signed-off-by: GitHub <noreply@github.com>
2023-02-14 20:35:22 +01:00
382 changed files with 13144 additions and 2890 deletions

View File

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

View File

@ -80,6 +80,7 @@ jobs:
run: poetry run python -m lifecycle.migrate
test-unittest:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- name: Setup authentik env
@ -94,6 +95,7 @@ jobs:
flags: unit
test-integration:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- name: Setup authentik env
@ -111,6 +113,7 @@ jobs:
test-e2e:
name: test-e2e (${{ matrix.job.name }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:

View File

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Delete 'dev' containers older than a week
uses: snok/container-retention-policy@v1
uses: snok/container-retention-policy@v2
with:
image-names: dev-server,dev-ldap,dev-proxy
cut-off: One week ago UTC

3
.gitignore vendored
View File

@ -200,3 +200,6 @@ media/
.idea/
/gen-*/
data/
# Local Netlify folder
.netlify

20
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
"recommendations": [
"EditorConfig.EditorConfig",
"bashmish.es6-string-css",
"bpruitt-goddard.mermaid-markdown-syntax-highlighting",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"golang.go",
"Gruntfuggly.todo-tree",
"mechatroner.rainbow-csv",
"ms-python.black-formatter",
"ms-python.isort",
"ms-python.pylint",
"ms-python.python",
"ms-python.vscode-pylance",
"redhat.vscode-yaml",
"Tobermory.es6-string-html",
"unifiedjs.vscode-mdx"
]
}

View File

@ -16,7 +16,8 @@
"passwordless",
"kubernetes",
"sso",
"slo"
"slo",
"scim",
],
"python.linting.pylintEnabled": true,
"todo-tree.tree.showCountsInTree": true,

View File

@ -154,12 +154,19 @@ While the prerequisites above must be satisfied prior to having your pull reques
## Styleguides
### PR naming
- Use the format of `<package>: <verb> <description>`
- See [here](#authentik-packages) for `package`
- Example: `providers/saml2: fix parsing of requests`
### Git Commit Messages
- Use the format of `<package>: <verb> <description>`
- See [here](#authentik-packages) for `package`
- Example: `providers/saml2: fix parsing of requests`
- Reference issues and pull requests liberally after the first line
- Naming of commits within a PR does not need to adhere to the guidelines as we squash merge PRs
### Python Styleguide

View File

@ -31,7 +31,7 @@ RUN pip install --no-cache-dir poetry && \
poetry export -f requirements.txt --dev --output requirements-dev.txt
# Stage 4: Build go proxy
FROM docker.io/golang:1.20.0-bullseye AS go-builder
FROM docker.io/golang:1.20.2-bullseye AS go-builder
WORKDIR /work
@ -96,7 +96,7 @@ RUN apt-get update && \
COPY ./authentik/ /authentik
COPY ./pyproject.toml /
COPY ./xml /xml
COPY ./schemas /schemas
COPY ./locale /locale
COPY ./tests /tests
COPY ./manage.py /

View File

@ -8,6 +8,7 @@ Authentik takes security very seriously. We follow the rules of [responsible dis
| --------- | ------------------ |
| 2022.12.x | :white_check_mark: |
| 2023.1.x | :white_check_mark: |
| 2023.2.x | :white_check_mark: |
## Reporting a Vulnerability

View File

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

View File

@ -9,6 +9,7 @@ from authentik.blueprints.tests import reconcile_app
from authentik.core.models import Group, User
from authentik.core.tasks import clean_expired_models
from authentik.events.monitored_tasks import TaskResultStatus
from authentik.lib.generators import generate_id
class TestAdminAPI(TestCase):
@ -16,8 +17,8 @@ class TestAdminAPI(TestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create(username="test-user")
self.group = Group.objects.create(name="superusers", is_superuser=True)
self.user = User.objects.create(username=generate_id())
self.group = Group.objects.create(name=generate_id(), is_superuser=True)
self.group.users.add(self.user)
self.group.save()
self.client.force_login(self.user)

View File

@ -4,6 +4,7 @@ from base64 import b64encode
from django.conf import settings
from django.test import TestCase
from django.utils import timezone
from rest_framework.exceptions import AuthenticationFailed
from authentik.api.authentication import bearer_auth
@ -68,6 +69,7 @@ class TestAPIAuth(TestCase):
user=create_test_admin_user(),
provider=provider,
token=generate_id(),
auth_time=timezone.now(),
_scope=SCOPE_AUTHENTIK_API,
_id_token=json.dumps({}),
)
@ -82,6 +84,7 @@ class TestAPIAuth(TestCase):
user=create_test_admin_user(),
provider=provider,
token=generate_id(),
auth_time=timezone.now(),
_scope="",
_id_token=json.dumps({}),
)

View File

@ -4,6 +4,7 @@ from guardian.shortcuts import assign_perm
from rest_framework.test import APITestCase
from authentik.core.models import Application, User
from authentik.lib.generators import generate_id
class TestAPIDecorators(APITestCase):
@ -16,7 +17,7 @@ class TestAPIDecorators(APITestCase):
def test_obj_perm_denied(self):
"""Test object perm denied"""
self.client.force_login(self.user)
app = Application.objects.create(name="denied", slug="denied")
app = Application.objects.create(name=generate_id(), slug=generate_id())
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
)
@ -25,7 +26,7 @@ class TestAPIDecorators(APITestCase):
def test_other_perm_denied(self):
"""Test other perm denied"""
self.client.force_login(self.user)
app = Application.objects.create(name="denied", slug="denied")
app = Application.objects.create(name=generate_id(), slug=generate_id())
assign_perm("authentik_core.view_application", self.user, app)
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})

View File

@ -58,6 +58,8 @@ from authentik.providers.oauth2.api.tokens import (
from authentik.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet
from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet
from authentik.providers.saml.api.providers import SAMLProviderViewSet
from authentik.providers.scim.api.property_mapping import SCIMMappingViewSet
from authentik.providers.scim.api.providers import SCIMProviderViewSet
from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
from authentik.sources.oauth.api.source import OAuthSourceViewSet
from authentik.sources.oauth.api.source_connection import UserOAuthSourceConnectionViewSet
@ -163,6 +165,7 @@ router.register("providers/ldap", LDAPProviderViewSet)
router.register("providers/proxy", ProxyProviderViewSet)
router.register("providers/oauth2", OAuth2ProviderViewSet)
router.register("providers/saml", SAMLProviderViewSet)
router.register("providers/scim", SCIMProviderViewSet)
router.register("oauth2/authorization_codes", AuthorizationCodeViewSet)
router.register("oauth2/refresh_tokens", RefreshTokenViewSet)
@ -173,6 +176,7 @@ router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
router.register("propertymappings/scope", ScopeMappingViewSet)
router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
router.register("propertymappings/scim", SCIMMappingViewSet)
router.register("authenticators/all", DeviceViewSet, basename="device")
router.register("authenticators/duo", DuoDeviceViewSet)

View File

@ -37,7 +37,6 @@ from authentik.lib.utils.file import (
from authentik.policies.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult
from authentik.stages.user_login.stage import USER_LOGIN_AUTHENTICATED
LOGGER = get_logger()
@ -186,10 +185,6 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
if superuser_full_list and request.user.is_superuser:
return super().list(request)
# To prevent the user from having to double login when prompt is set to login
# and the user has just signed it. This session variable is set in the UserLoginStage
# and is (quite hackily) removed from the session in applications's API's List method
self.request.session.pop(USER_LOGIN_AUTHENTICATED, None)
queryset = self._filter_queryset_for_list(self.get_queryset())
self.paginate_queryset(queryset)

View File

@ -24,7 +24,6 @@ from authentik.core.models import Group, User
class GroupMemberSerializer(ModelSerializer):
"""Stripped down user serializer to show relevant users for groups"""
avatar = CharField(read_only=True)
attributes = JSONField(validators=[is_dict], required=False)
uid = CharField(read_only=True)
@ -37,7 +36,6 @@ class GroupMemberSerializer(ModelSerializer):
"is_active",
"last_login",
"email",
"avatar",
"attributes",
"uid",
]

View File

@ -44,6 +44,9 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
"verbose_name_plural",
"meta_model_name",
]
extra_kwargs = {
"authorization_flow": {"required": True, "allow_null": False},
}
class ProviderViewSet(

View File

@ -206,5 +206,6 @@ class UserSourceConnectionViewSet(
queryset = UserSourceConnection.objects.all()
serializer_class = UserSourceConnectionSerializer
permission_classes = [OwnerSuperuserPermissions]
filterset_fields = ["user"]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["pk"]

View File

@ -31,8 +31,14 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created."""
request: Request = self.context["request"]
attrs.setdefault("user", request.user)
request: Request = self.context.get("request")
if not request:
if "user" not in attrs:
raise ValidationError("Missing user")
if "intent" not in attrs:
raise ValidationError("Missing intent")
else:
attrs.setdefault("user", request.user)
attrs.setdefault("intent", TokenIntents.INTENT_API)
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
raise ValidationError(f"Invalid intent {attrs.get('intent')}")

View File

@ -38,6 +38,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import (
BooleanField,
DateTimeField,
ListSerializer,
ModelSerializer,
PrimaryKeyRelatedField,
@ -67,6 +68,7 @@ from authentik.core.models import (
User,
)
from authentik.events.models import EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
from authentik.flows.views.executor import QS_KEY_TOKEN
@ -325,12 +327,16 @@ class UserViewSet(UsedByMixin, ModelViewSet):
user: User = self.get_object()
planner = FlowPlanner(flow)
planner.allow_empty_flows = True
plan = planner.plan(
self.request._request,
{
PLAN_CONTEXT_PENDING_USER: user,
},
)
try:
plan = planner.plan(
self.request._request,
{
PLAN_CONTEXT_PENDING_USER: user,
},
)
except FlowNonApplicableException:
LOGGER.warning("Recovery flow not applicable to user")
return None, None
token, __ = FlowToken.objects.update_or_create(
identifier=f"{user.uid}-password-reset",
defaults={
@ -353,6 +359,11 @@ class UserViewSet(UsedByMixin, ModelViewSet):
{
"name": CharField(required=True),
"create_group": BooleanField(default=False),
"expiring": BooleanField(default=True),
"expires": DateTimeField(
required=False,
help_text="If not provided, valid for 360 days",
),
},
),
responses={
@ -373,14 +384,20 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"""Create a new user account that is marked as a service account"""
username = request.data.get("name")
create_group = request.data.get("create_group", False)
expiring = request.data.get("expiring", True)
expires = request.data.get("expires", now() + timedelta(days=360))
with atomic():
try:
user = User.objects.create(
user: User = User.objects.create(
username=username,
name=username,
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: expiring},
path=USER_PATH_SERVICE_ACCOUNT,
)
user.set_unusable_password()
user.save()
response = {
"username": user.username,
"user_uid": user.uid,
@ -396,7 +413,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
identifier=slugify(f"service-account-{username}-password"),
intent=TokenIntents.INTENT_APP_PASSWORD,
user=user,
expires=now() + timedelta(days=360),
expires=expires,
expiring=expiring,
)
response["token"] = token.key
return Response(response)

View File

@ -1,8 +1,9 @@
"""Property Mapping Evaluator"""
from typing import Optional
from typing import Any, Optional
from django.db.models import Model
from django.http import HttpRequest
from prometheus_client import Histogram
from authentik.core.models import User
from authentik.events.models import Event, EventAction
@ -10,6 +11,12 @@ from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.types import PolicyRequest
PROPERTY_MAPPING_TIME = Histogram(
"authentik_property_mapping_execution_time",
"Evaluation time of property mappings",
["mapping_name"],
)
class PropertyMappingEvaluator(BaseEvaluator):
"""Custom Evaluator that adds some different context variables."""
@ -49,3 +56,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
event.from_http(req.http_request, req.user)
return
event.save()
def evaluate(self, *args, **kwargs) -> Any:
with PROPERTY_MAPPING_TIME.labels(mapping_name=self._filename).time():
return super().evaluate(*args, **kwargs)

View File

@ -18,13 +18,13 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
akadmin, _ = User.objects.using(db_alias).get_or_create(
username="akadmin", email="root@localhost", name="authentik Default Admin"
username="akadmin",
email=environ.get("AUTHENTIK_BOOTSTRAP_EMAIL", "root@localhost"),
name="authentik Default Admin",
)
password = None
if "TF_BUILD" in environ or settings.TEST:
password = "akadmin" # noqa # nosec
if "AK_ADMIN_PASS" in environ:
password = environ["AK_ADMIN_PASS"]
if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ:
password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]
if password:

View File

@ -46,13 +46,9 @@ def create_default_user_token(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
akadmin = User.objects.using(db_alias).filter(username="akadmin")
if not akadmin.exists():
return
key = None
if "AK_ADMIN_TOKEN" in environ:
key = environ["AK_ADMIN_TOKEN"]
if "AUTHENTIK_BOOTSTRAP_TOKEN" in environ:
key = environ["AUTHENTIK_BOOTSTRAP_TOKEN"]
if not key:
if "AUTHENTIK_BOOTSTRAP_TOKEN" not in environ:
return
key = environ["AUTHENTIK_BOOTSTRAP_TOKEN"]
Token.objects.using(db_alias).create(
identifier="authentik-bootstrap-token",
user=akadmin.first(),
@ -186,7 +182,9 @@ class Migration(migrations.Migration):
model_name="application",
name="meta_launch_url",
field=models.TextField(
blank=True, default="", validators=[authentik.lib.models.DomainlessURLValidator()]
blank=True,
default="",
validators=[authentik.lib.models.DomainlessFormattedURLValidator()],
),
),
migrations.RunPython(

View File

@ -0,0 +1,25 @@
# Generated by Django 4.1.7 on 2023-03-02 21:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"),
("authentik_core", "0024_source_icon"),
]
operations = [
migrations.AlterField(
model_name="provider",
name="authorization_flow",
field=models.ForeignKey(
help_text="Flow used when authorizing this provider.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="provider_authorization",
to="authentik_flows.flow",
),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 4.1.7 on 2023-03-07 13:41
from django.db import migrations, models
from authentik.lib.migrations import fallback_names
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0025_alter_provider_authorization_flow"),
]
operations = [
migrations.RunPython(fallback_names("authentik_core", "propertymapping", "name")),
migrations.RunPython(fallback_names("authentik_core", "provider", "name")),
migrations.AlterField(
model_name="propertymapping",
name="name",
field=models.TextField(unique=True),
),
migrations.AlterField(
model_name="provider",
name="name",
field=models.TextField(unique=True),
),
]

View File

@ -22,12 +22,15 @@ from structlog.stdlib import get_logger
from authentik.blueprints.models import ManagedModel
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.signals import password_changed
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.lib.avatars import get_avatar
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
from authentik.lib.models import CreatedUpdatedModel, DomainlessURLValidator, SerializerModel
from authentik.lib.models import (
CreatedUpdatedModel,
DomainlessFormattedURLValidator,
SerializerModel,
)
from authentik.lib.utils.http import get_client_ip
from authentik.policies.models import PolicyBindingModel
@ -189,6 +192,8 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
def set_password(self, raw_password, signal=True):
if self.pk and signal:
from authentik.core.signals import password_changed
password_changed.send(sender=self, user=self, password=raw_password)
self.password_change_date = now()
return super().set_password(raw_password)
@ -242,11 +247,12 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
class Provider(SerializerModel):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
name = models.TextField()
name = models.TextField(unique=True)
authorization_flow = models.ForeignKey(
"authentik_flows.Flow",
on_delete=models.CASCADE,
null=True,
help_text=_("Flow used when authorizing this provider."),
related_name="provider_authorization",
)
@ -289,7 +295,7 @@ class Application(SerializerModel, PolicyBindingModel):
)
meta_launch_url = models.TextField(
default="", blank=True, validators=[DomainlessURLValidator()]
default="", blank=True, validators=[DomainlessFormattedURLValidator()]
)
open_in_new_tab = models.BooleanField(
@ -606,7 +612,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
pm_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
name = models.TextField()
name = models.TextField(unique=True)
expression = models.TextField()
objects = InheritanceManager()
@ -629,7 +635,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
try:
return evaluator.evaluate(self.expression)
except Exception as exc:
raise PropertyMappingExpressionException(str(exc)) from exc
raise PropertyMappingExpressionException(exc) from exc
def __str__(self):
return f"Property Mapping {self.name}"

View File

@ -10,25 +10,25 @@ from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.http.request import HttpRequest
from authentik.core.models import Application, AuthenticatedSession
# Arguments: user: User, password: str
password_changed = Signal()
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
login_failed = Signal()
if TYPE_CHECKING:
from authentik.core.models import AuthenticatedSession, User
from authentik.core.models import User
@receiver(post_save)
@receiver(post_save, sender=Application)
def post_save_application(sender: type[Model], instance, created: bool, **_):
"""Clear user's application cache upon application creation"""
from authentik.core.api.applications import user_app_cache_key
from authentik.core.models import Application
if sender != Application:
return
if not created: # pragma: no cover
return
# Also delete user application cache
keys = cache.keys(user_app_cache_key("*"))
cache.delete_many(keys)
@ -37,7 +37,6 @@ def post_save_application(sender: type[Model], instance, created: bool, **_):
@receiver(user_logged_in)
def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
"""Create an AuthenticatedSession from request"""
from authentik.core.models import AuthenticatedSession
session = AuthenticatedSession.from_request(request, user)
if session:
@ -47,18 +46,11 @@ def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
@receiver(user_logged_out)
def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
"""Delete AuthenticatedSession if it exists"""
from authentik.core.models import AuthenticatedSession
AuthenticatedSession.objects.filter(session_key=request.session.session_key).delete()
@receiver(pre_delete)
@receiver(pre_delete, sender=AuthenticatedSession)
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
"""Delete session when authenticated session is deleted"""
from authentik.core.models import AuthenticatedSession
if sender != AuthenticatedSession:
return
cache_key = f"{KEY_PREFIX}{instance.session_key}"
cache.delete(cache_key)

View File

@ -16,7 +16,8 @@
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
<script src="{% static 'dist/poly.js' %}" type="module"></script>
{% block head %}
{% endblock %}

View File

@ -37,6 +37,22 @@ class TestApplicationsAPI(APITestCase):
order=0,
)
def test_formatted_launch_url(self):
"""Test formatted launch URL"""
self.client.force_login(self.user)
self.assertEqual(
self.client.patch(
reverse("authentik_api:application-detail", kwargs={"slug": self.allowed.slug}),
{"meta_launch_url": "https://%(username)s.test.goauthentik.io/%(username)s"},
).status_code,
200,
)
self.allowed.refresh_from_db()
self.assertEqual(
self.allowed.get_launch_url(self.user),
f"https://{self.user.username}.test.goauthentik.io/{self.user.username}",
)
def test_set_icon(self):
"""Test set_icon"""
file = ContentFile(b"text", "name")

View File

@ -5,6 +5,7 @@ from django.urls.base import reverse
from guardian.shortcuts import get_anonymous_user
from rest_framework.test import APITestCase
from authentik.core.api.tokens import TokenSerializer
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
@ -99,3 +100,16 @@ class TestTokenAPI(APITestCase):
self.assertEqual(len(body["results"]), 2)
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
self.assertEqual(body["results"][1]["identifier"], token_should_not.identifier)
def test_serializer_no_request(self):
"""Test serializer without request"""
self.assertTrue(
TokenSerializer(
data={
"identifier": generate_id(),
"intent": TokenIntents.INTENT_APP_PASSWORD,
"key": generate_id(),
"user": self.user.pk,
}
).is_valid(raise_exception=True)
)

View File

@ -1,11 +1,19 @@
"""Test Users API"""
from datetime import datetime
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.urls.base import reverse
from rest_framework.test import APITestCase
from authentik.core.models import AuthenticatedSession, User
from authentik.core.models import (
USER_ATTRIBUTE_SA,
USER_ATTRIBUTE_TOKEN_EXPIRING,
AuthenticatedSession,
Token,
User,
)
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
from authentik.flows.models import FlowDesignation
from authentik.lib.generators import generate_id, generate_key
@ -130,7 +138,71 @@ class TestUsersAPI(APITestCase):
},
)
self.assertEqual(response.status_code, 200)
self.assertTrue(User.objects.filter(username="test-sa").exists())
user_filter = User.objects.filter(
username="test-sa",
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
)
self.assertTrue(user_filter.exists())
user: User = user_filter.first()
self.assertFalse(user.has_usable_password())
token_filter = Token.objects.filter(user=user)
self.assertTrue(token_filter.exists())
self.assertTrue(token_filter.first().expiring)
def test_service_account_no_expire(self):
"""Service account creation without token expiration"""
self.client.force_login(self.admin)
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "test-sa",
"create_group": True,
"expiring": False,
},
)
self.assertEqual(response.status_code, 200)
user_filter = User.objects.filter(
username="test-sa",
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: False, USER_ATTRIBUTE_SA: True},
)
self.assertTrue(user_filter.exists())
user: User = user_filter.first()
self.assertFalse(user.has_usable_password())
token_filter = Token.objects.filter(user=user)
self.assertTrue(token_filter.exists())
self.assertFalse(token_filter.first().expiring)
def test_service_account_with_custom_expire(self):
"""Service account creation with custom token expiration date"""
self.client.force_login(self.admin)
expire_on = datetime(2050, 11, 11, 11, 11, 11).astimezone()
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "test-sa",
"create_group": True,
"expires": expire_on.isoformat(),
},
)
self.assertEqual(response.status_code, 200)
user_filter = User.objects.filter(
username="test-sa",
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
)
self.assertTrue(user_filter.exists())
user: User = user_filter.first()
self.assertFalse(user.has_usable_password())
token_filter = Token.objects.filter(user=user)
self.assertTrue(token_filter.exists())
token = token_filter.first()
self.assertTrue(token.expiring)
self.assertEqual(token.expires, expire_on)
def test_service_account_invalid(self):
"""Service account creation (twice with same name, expect error)"""
@ -143,7 +215,19 @@ class TestUsersAPI(APITestCase):
},
)
self.assertEqual(response.status_code, 200)
self.assertTrue(User.objects.filter(username="test-sa").exists())
user_filter = User.objects.filter(
username="test-sa",
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
)
self.assertTrue(user_filter.exists())
user: User = user_filter.first()
self.assertFalse(user.has_usable_password())
token_filter = Token.objects.filter(user=user)
self.assertTrue(token_filter.exists())
self.assertTrue(token_filter.first().expiring)
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={

View File

@ -11,6 +11,7 @@ from authentik.flows.challenge import (
HttpChallengeResponse,
RedirectChallenge,
)
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import ChallengeStageView
@ -41,15 +42,18 @@ class RedirectToAppLaunch(View):
flow = tenant.flow_authentication
planner = FlowPlanner(flow)
planner.allow_empty_flows = True
plan = planner.plan(
request,
{
PLAN_CONTEXT_APPLICATION: app,
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": app.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
},
)
try:
plan = planner.plan(
request,
{
PLAN_CONTEXT_APPLICATION: app,
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": app.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
},
)
except FlowNonApplicableException:
raise Http404
plan.insert_stage(in_memory_stage(RedirectToAppStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)

View File

@ -7,13 +7,14 @@ from django.conf import settings
from django.contrib.sessions.models import Session
from django.core.exceptions import SuspiciousOperation
from django.db.models import Model
from django.db.models.signals import post_save, pre_delete
from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.http import HttpRequest, HttpResponse
from django_otp.plugins.otp_static.models import StaticToken
from guardian.models import UserObjectPermission
from authentik.core.models import (
AuthenticatedSession,
Group,
PropertyMapping,
Provider,
Source,
@ -28,6 +29,7 @@ from authentik.lib.utils.errors import exception_to_string
from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
from authentik.providers.scim.models import SCIMGroup, SCIMUser
IGNORED_MODELS = (
Event,
@ -48,6 +50,8 @@ IGNORED_MODELS = (
AuthorizationCode,
AccessToken,
RefreshToken,
SCIMUser,
SCIMGroup,
)
@ -58,6 +62,13 @@ def should_log_model(model: Model) -> bool:
return model.__class__ not in IGNORED_MODELS
def should_log_m2m(model: Model) -> bool:
"""Return true if m2m operation should be logged"""
if model.__class__ in [User, Group]:
return True
return False
class EventNewThread(Thread):
"""Create Event in background thread"""
@ -96,6 +107,7 @@ class AuditMiddleware:
return
post_save_handler = partial(self.post_save_handler, user=request.user, request=request)
pre_delete_handler = partial(self.pre_delete_handler, user=request.user, request=request)
m2m_changed_handler = partial(self.m2m_changed_handler, user=request.user, request=request)
post_save.connect(
post_save_handler,
dispatch_uid=request.request_id,
@ -106,6 +118,11 @@ class AuditMiddleware:
dispatch_uid=request.request_id,
weak=False,
)
m2m_changed.connect(
m2m_changed_handler,
dispatch_uid=request.request_id,
weak=False,
)
def disconnect(self, request: HttpRequest):
"""Disconnect signals"""
@ -113,6 +130,7 @@ class AuditMiddleware:
return
post_save.disconnect(dispatch_uid=request.request_id)
pre_delete.disconnect(dispatch_uid=request.request_id)
m2m_changed.disconnect(dispatch_uid=request.request_id)
def __call__(self, request: HttpRequest) -> HttpResponse:
self.connect(request)
@ -167,3 +185,20 @@ class AuditMiddleware:
user=user,
model=model_to_dict(instance),
).run()
@staticmethod
def m2m_changed_handler(
user: User, request: HttpRequest, sender, instance: Model, action: str, **_
):
"""Signal handler for all object's m2m_changed"""
if action not in ["pre_add", "pre_remove", "post_clear"]:
return
if not should_log_m2m(instance):
return
EventNewThread(
EventAction.MODEL_UPDATED,
request,
user=user,
model=model_to_dict(instance),
).run()

View File

@ -2,7 +2,6 @@
import uuid
from datetime import timedelta
from typing import Iterable
import django.db.models.deletion
from django.apps.registry import Apps
@ -13,6 +12,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
import authentik.events.models
import authentik.lib.models
from authentik.events.models import EventAction, NotificationSeverity, TransportMode
from authentik.lib.migrations import progress_bar
def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
@ -43,49 +43,6 @@ def token_view_to_secret_view(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
Event.objects.using(db_alias).bulk_update(events, ["context", "action"])
# Taken from https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console
def progress_bar(
iterable: Iterable,
prefix="Writing: ",
suffix=" finished",
decimals=1,
length=100,
fill="",
print_end="\r",
):
"""
Call in a loop to create terminal progress bar
@params:
iteration - Required : current iteration (Int)
total - Required : total iterations (Int)
prefix - Optional : prefix string (Str)
suffix - Optional : suffix string (Str)
decimals - Optional : positive number of decimals in percent complete (Int)
length - Optional : character length of bar (Int)
fill - Optional : bar fill character (Str)
print_end - Optional : end character (e.g. "\r", "\r\n") (Str)
"""
total = len(iterable)
if total < 1:
return
def print_progress_bar(iteration):
"""Progress Bar Printing Function"""
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
filledLength = int(length * iteration // total)
bar = fill * filledLength + "-" * (length - filledLength)
print(f"\r{prefix} |{bar}| {percent}% {suffix}", end=print_end)
# Initial Call
print_progress_bar(0)
# Update Progress Bar
for i, item in enumerate(iterable):
yield item
print_progress_bar(i + 1)
# Print New Line on Complete
print()
def update_expires(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
Event = apps.get_model("authentik_events", "event")

View File

@ -111,6 +111,7 @@ class MonitoredTask(Task):
_result: Optional[TaskResult]
_uid: Optional[str]
start: Optional[float] = None
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
@ -118,7 +119,6 @@ class MonitoredTask(Task):
self._uid = None
self._result = None
self.result_timeout_hours = 6
self.start = default_timer()
def set_uid(self, uid: str):
"""Set UID, so in the case of an unexpected error its saved correctly"""
@ -128,6 +128,10 @@ class MonitoredTask(Task):
"""Set result for current run, will overwrite previous result."""
self._result = result
def before_start(self, task_id, args, kwargs):
self.start = default_timer()
return super().before_start(task_id, args, kwargs)
# pylint: disable=too-many-arguments
def after_return(self, status, retval, task_id, args: list[Any], kwargs: dict[str, Any], einfo):
super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
@ -138,7 +142,7 @@ class MonitoredTask(Task):
info = TaskInfo(
task_name=self.__name__,
task_description=self.__doc__,
start_timestamp=self.start,
start_timestamp=self.start or default_timer(),
finish_timestamp=default_timer(),
finish_time=datetime.now(),
result=self._result,
@ -162,7 +166,7 @@ class MonitoredTask(Task):
TaskInfo(
task_name=self.__name__,
task_description=self.__doc__,
start_timestamp=self.start,
start_timestamp=self.start or default_timer(),
finish_timestamp=default_timer(),
finish_time=datetime.now(),
result=self._result,

View File

@ -1,4 +1,7 @@
"""Flow Binding API Views"""
from typing import Any
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
@ -12,6 +15,13 @@ class FlowStageBindingSerializer(ModelSerializer):
stage_obj = StageSerializer(read_only=True, source="stage")
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
evaluate_on_plan = attrs.get("evaluate_on_plan", False)
re_evaluate_policies = attrs.get("re_evaluate_policies", True)
if not evaluate_on_plan and not re_evaluate_policies:
raise ValidationError("Either evaluation on plan or evaluation on run must be enabled")
return super().validate(attrs)
class Meta:
model = FlowStageBinding
fields = [

View File

@ -8,7 +8,7 @@ from rest_framework.serializers import CharField
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User
from authentik.flows.models import Flow, FlowStageBinding
from authentik.flows.models import Flow, FlowAuthenticationRequirement, FlowStageBinding
@dataclass
@ -160,12 +160,37 @@ class FlowDiagram:
)
return stages + elements
def get_flow_auth_requirement(self) -> list[DiagramElement]:
"""Get flow authentication requirement"""
end_el = DiagramElement(
"done",
_("End of the flow"),
_("Requirement not fulfilled"),
style=["[[", "]]"],
)
elements = []
if self.flow.authentication == FlowAuthenticationRequirement.NONE:
return []
auth = DiagramElement(
"flow_auth_requirement",
_("Flow authentication requirement") + "\n" + self.flow.authentication,
)
elements.append(auth)
end_el.source = [auth]
elements.append(end_el)
elements.append(
DiagramElement("flow_start", "placeholder", _("Requirement fulfilled"), source=[auth])
)
return elements
def build(self) -> str:
"""Build flowchart"""
all_elements = [
"graph TD",
]
all_elements.extend(self.get_flow_auth_requirement())
pre_flow_policies_element = DiagramElement(
"flow_pre", _("Pre-flow policies"), style=["[[", "]]"]
)
@ -179,6 +204,7 @@ class FlowDiagram:
_("End of the flow"),
_("Policy denied"),
flow_policies,
style=["[[", "]]"],
)
)

View File

@ -0,0 +1,26 @@
# Generated by Django 4.1.7 on 2023-02-25 15:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0024_flow_authentication"),
]
operations = [
migrations.AlterField(
model_name="flowstagebinding",
name="evaluate_on_plan",
field=models.BooleanField(
default=False, help_text="Evaluate policies during the Flow planning process."
),
),
migrations.AlterField(
model_name="flowstagebinding",
name="re_evaluate_policies",
field=models.BooleanField(
default=True, help_text="Evaluate policies when the Stage is present to the user."
),
),
]

View File

@ -211,14 +211,11 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
stage = InheritanceForeignKey(Stage, on_delete=models.CASCADE)
evaluate_on_plan = models.BooleanField(
default=True,
help_text=_(
"Evaluate policies during the Flow planning process. "
"Disable this for input-based policies."
),
default=False,
help_text=_("Evaluate policies during the Flow planning process."),
)
re_evaluate_policies = models.BooleanField(
default=False,
default=True,
help_text=_("Evaluate policies when the Stage is present to the user."),
)

View File

@ -147,7 +147,6 @@ class FlowPlanner:
) -> FlowPlan:
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
and return ordered list"""
self._check_authentication(request)
with Hub.current.start_span(
op="authentik.flow.planner.plan", description=self.flow.slug
) as span:
@ -165,6 +164,12 @@ class FlowPlanner:
user = default_context[PLAN_CONTEXT_PENDING_USER]
else:
user = request.user
# We only need to check the flow authentication if it's planned without a user
# in the context, as a user in the context can only be set via the explicit code API
# or if a flow is restarted due to `invalid_response_action` being set to
# `restart_with_context`, which can only happen if the user was already authorized
# to use the flow
self._check_authentication(request)
# First off, check the flow's direct policy bindings
# to make sure the user even has access to the flow
engine = PolicyEngine(self.flow, user, request)
@ -261,7 +266,6 @@ class FlowPlanner:
marker = ReevaluateMarker(binding=binding)
if stage:
plan.append(binding, marker)
HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug)
self._logger.debug(
"f(plan): finished building",
)

View File

@ -7,6 +7,7 @@ from django.http.request import QueryDict
from django.http.response import HttpResponse
from django.urls import reverse
from django.views.generic.base import View
from prometheus_client import Histogram
from rest_framework.request import Request
from sentry_sdk.hub import Hub
from structlog.stdlib import BoundLogger, get_logger
@ -31,6 +32,11 @@ if TYPE_CHECKING:
from authentik.flows.views.executor import FlowExecutorView
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
HIST_FLOWS_STAGE_TIME = Histogram(
"authentik_flows_stage_time",
"Duration taken by different parts of stages",
["stage_type", "method"],
)
class StageView(View):
@ -109,14 +115,24 @@ class ChallengeStageView(StageView):
keep_context=keep_context,
)
return self.executor.restart_flow(keep_context)
with Hub.current.start_span(
op="authentik.flow.stage.challenge_invalid",
description=self.__class__.__name__,
with (
Hub.current.start_span(
op="authentik.flow.stage.challenge_invalid",
description=self.__class__.__name__,
),
HIST_FLOWS_STAGE_TIME.labels(
stage_type=self.__class__.__name__, method="challenge_invalid"
).time(),
):
return self.challenge_invalid(challenge)
with Hub.current.start_span(
op="authentik.flow.stage.challenge_valid",
description=self.__class__.__name__,
with (
Hub.current.start_span(
op="authentik.flow.stage.challenge_valid",
description=self.__class__.__name__,
),
HIST_FLOWS_STAGE_TIME.labels(
stage_type=self.__class__.__name__, method="challenge_valid"
).time(),
):
return self.challenge_valid(challenge)
@ -135,9 +151,14 @@ class ChallengeStageView(StageView):
return self.executor.flow.title
def _get_challenge(self, *args, **kwargs) -> Challenge:
with Hub.current.start_span(
op="authentik.flow.stage.get_challenge",
description=self.__class__.__name__,
with (
Hub.current.start_span(
op="authentik.flow.stage.get_challenge",
description=self.__class__.__name__,
),
HIST_FLOWS_STAGE_TIME.labels(
stage_type=self.__class__.__name__, method="get_challenge"
).time(),
):
challenge = self.get_challenge(*args, **kwargs)
with Hub.current.start_span(
@ -210,7 +231,7 @@ class AccessDeniedChallengeView(ChallengeStageView):
def get_challenge(self, *args, **kwargs) -> Challenge:
return AccessDeniedChallenge(
data={
"error_message": self.error_message or "Unknown error",
"error_message": str(self.error_message or "Unknown error"),
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-access-denied",
}

View File

@ -162,7 +162,7 @@ class FlowExecutorView(APIView):
token.delete()
if not isinstance(plan, FlowPlan):
return None
plan.context[PLAN_CONTEXT_IS_RESTORED] = True
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
self._logger.debug("f(exec): restored flow plan from token", plan=plan)
return plan
@ -561,9 +561,13 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
LOGGER.debug("Stage has no configure_flow set", stage=stage)
raise Http404
plan = FlowPlanner(stage.configure_flow).plan(
request, {PLAN_CONTEXT_PENDING_USER: request.user}
)
try:
plan = FlowPlanner(stage.configure_flow).plan(
request, {PLAN_CONTEXT_PENDING_USER: request.user}
)
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
raise Http404
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",

View File

@ -1,10 +1,11 @@
"""Avatar utils"""
from base64 import b64encode
from functools import cache
from functools import cache as funccache
from hashlib import md5
from typing import TYPE_CHECKING, Optional
from urllib.parse import urlencode
from django.core.cache import cache
from django.templatetags.static import static
from lxml import etree # nosec
from lxml.etree import Element, SubElement # nosec
@ -15,6 +16,7 @@ from authentik.lib.utils.http import get_http_session
GRAVATAR_URL = "https://secure.gravatar.com"
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
CACHE_KEY_GRAVATAR = "goauthentik.io/lib/avatars/"
if TYPE_CHECKING:
from authentik.core.models import User
@ -50,22 +52,24 @@ def avatar_mode_gravatar(user: "User", mode: str) -> Optional[str]:
parameters = [("size", "158"), ("rating", "g"), ("default", "404")]
gravatar_url = f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}"
@cache
def check_non_default(url: str):
"""Cache HEAD check, based on URL"""
try:
# Since we specify a default of 404, do a HEAD request
# (HEAD since we don't need the body)
# so if that returns a 404, move onto the next mode
res = get_http_session().head(url, timeout=5)
if res.status_code == 404:
return None
res.raise_for_status()
except RequestException:
return url
return url
full_key = CACHE_KEY_GRAVATAR + mail_hash
if cache.has_key(full_key):
cache.touch(full_key)
return cache.get(full_key)
return check_non_default(gravatar_url)
try:
# Since we specify a default of 404, do a HEAD request
# (HEAD since we don't need the body)
# so if that returns a 404, move onto the next mode
res = get_http_session().head(gravatar_url, timeout=5)
if res.status_code == 404:
cache.set(full_key, None)
return None
res.raise_for_status()
except RequestException:
return gravatar_url
cache.set(full_key, gravatar_url)
return gravatar_url
def generate_colors(text: str) -> tuple[str, str]:
@ -83,10 +87,10 @@ def generate_colors(text: str) -> tuple[str, str]:
return bg_hex, text_hex
@cache
@funccache
# pylint: disable=too-many-arguments,too-many-locals
def generate_avatar_from_name(
user: "User",
name: str,
length: int = 2,
size: int = 64,
rounded: bool = False,
@ -98,8 +102,6 @@ def generate_avatar_from_name(
Inspired from: https://github.com/LasseRafn/ui-avatars
"""
name = user.name if user.name != "" else "a k"
name_parts = name.split()
# Only abbreviate first and last name
if len(name_parts) > 2:
@ -152,7 +154,7 @@ def generate_avatar_from_name(
def avatar_mode_generated(user: "User", mode: str) -> Optional[str]:
"""Wrapper that converts generated avatar to base64 svg"""
svg = generate_avatar_from_name(user)
svg = generate_avatar_from_name(user.name if user.name.strip() != "" else "a k")
return f"data:image/svg+xml;base64,{b64encode(svg.encode('utf-8')).decode('utf-8')}"

View File

@ -1,9 +1,11 @@
"""authentik expression policy evaluator"""
import re
import socket
from ipaddress import ip_address, ip_network
from textwrap import indent
from typing import Any, Iterable, Optional
from cachetools import TLRUCache, cached
from django.core.exceptions import FieldError
from django_otp import devices_for_user
from rest_framework.serializers import ValidationError
@ -41,6 +43,8 @@ class BaseEvaluator:
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
"ak_user_by": BaseEvaluator.expr_user_by,
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
"resolve_dns": BaseEvaluator.expr_resolve_dns,
"reverse_dns": BaseEvaluator.expr_reverse_dns,
"ak_create_event": self.expr_event_create,
"ak_logger": get_logger(self._filename).bind(),
"requests": get_http_session(),
@ -49,6 +53,39 @@ class BaseEvaluator:
}
self._context = {}
@cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))
@staticmethod
def expr_resolve_dns(host: str, ip_version: Optional[int] = None) -> list[str]:
"""Resolve host to a list of IPv4 and/or IPv6 addresses."""
# Although it seems to be fine (raising OSError), docs warn
# against passing `None` for both the host and the port
# https://docs.python.org/3/library/socket.html#socket.getaddrinfo
host = host or ""
ip_list = []
family = 0
if ip_version == 4:
family = socket.AF_INET
if ip_version == 6:
family = socket.AF_INET6
try:
for ip_addr in socket.getaddrinfo(host, None, family=family):
ip_list.append(str(ip_addr[4][0]))
except OSError:
pass
return list(set(ip_list))
@cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))
@staticmethod
def expr_reverse_dns(ip_addr: str) -> str:
"""Perform a reverse DNS lookup."""
try:
return socket.getfqdn(ip_addr)
except OSError:
return ip_addr
@staticmethod
def expr_flatten(value: list[Any] | Any) -> Optional[Any]:
"""Flatten `value` if its a list"""

View File

@ -0,0 +1,58 @@
"""Migration helpers"""
from typing import Iterable
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def fallback_names(app: str, model: str, field: str):
"""Factory function that checks all instances of `app`.`model` instance's `field`
to prevent any duplicates"""
def migrator(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
klass = apps.get_model(app, model)
seen_names = []
for obj in klass.objects.using(db_alias).all():
value = getattr(obj, field)
if value not in seen_names:
seen_names.append(value)
continue
new_value = value + "_2"
setattr(obj, field, new_value)
obj.save()
return migrator
def progress_bar(iterable: Iterable):
"""Call in a loop to create terminal progress bar
https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console"""
prefix = "Writing: "
suffix = " finished"
decimals = 1
length = 100
fill = ""
print_end = "\r"
total = len(iterable)
if total < 1:
return
def print_progress_bar(iteration):
"""Progress Bar Printing Function"""
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
filled_length = int(length * iteration // total)
bar = fill * filled_length + "-" * (length - filled_length)
print(f"\r{prefix} |{bar}| {percent}% {suffix}", end=print_end)
# Initial Call
print_progress_bar(0)
# Update Progress Bar
for i, item in enumerate(iterable):
yield item
print_progress_bar(i + 1)
# Print New Line on Complete
print()

View File

@ -74,3 +74,21 @@ class DomainlessURLValidator(URLValidator):
if scheme not in self.schemes:
value = "default" + value
super().__call__(value)
class DomainlessFormattedURLValidator(DomainlessURLValidator):
"""URL validator which allows for python format strings"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.host_re = r"([%\(\)a-zA-Z])+" + self.domain_re + self.domain_re
self.regex = _lazy_re_compile(
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
r"(?::\d{2,5})?" # port
r"(?:[/?#][^\s]*)?" # resource path
r"\Z",
re.IGNORECASE,
)
self.schemes = ["http", "https", "blank"] + list(self.schemes)

View File

@ -4,7 +4,6 @@ from typing import Any, Optional
from billiard.exceptions import SoftTimeLimitExceeded, WorkerLostError
from celery.exceptions import CeleryError
from channels.middleware import BaseMiddleware
from channels_redis.core import ChannelFull
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError
@ -17,37 +16,24 @@ from ldap3.core.exceptions import LDAPException
from redis.exceptions import ConnectionError as RedisConnectionError
from redis.exceptions import RedisError, ResponseError
from rest_framework.exceptions import APIException
from sentry_sdk import HttpTransport, Hub
from sentry_sdk import HttpTransport
from sentry_sdk import init as sentry_sdk_init
from sentry_sdk.api import set_tag
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.threading import ThreadingIntegration
from sentry_sdk.tracing import Transaction
from structlog.stdlib import get_logger
from websockets.exceptions import WebSocketException
from authentik import __version__, get_build_hash
from authentik.lib.config import CONFIG
from authentik.lib.utils.http import authentik_user_agent
from authentik.lib.utils.reflection import class_to_path, get_env
from authentik.lib.utils.reflection import get_env
LOGGER = get_logger()
class SentryWSMiddleware(BaseMiddleware):
"""Sentry Websocket middleweare to set the transaction name based on
consumer class path"""
async def __call__(self, scope, receive, send):
transaction: Optional[Transaction] = Hub.current.scope.transaction
class_path = class_to_path(self.inner.consumer_class)
if transaction:
transaction.name = class_path
return await self.inner(scope, receive, send)
class SentryIgnoredException(Exception):
"""Base Class for all errors that are suppressed, and not sent to sentry."""
@ -94,9 +80,12 @@ def sentry_init(**sentry_init_kwargs):
def traces_sampler(sampling_context: dict) -> float:
"""Custom sampler to ignore certain routes"""
path = sampling_context.get("asgi_scope", {}).get("path", "")
_type = sampling_context.get("asgi_scope", {}).get("type", "")
# Ignore all healthcheck routes
if path.startswith("/-/health") or path.startswith("/-/metrics"):
return 0
if _type == "websocket":
return 0
return float(CONFIG.y("error_reporting.sample_rate", 0.1))

View File

@ -9,4 +9,4 @@ def get_lxml_parser():
def lxml_from_string(text: str):
"""Wrapper around fromstring"""
return fromstring(text, parser=get_lxml_parser())
return fromstring(text, parser=get_lxml_parser()) # nosec

View File

@ -16,7 +16,6 @@ from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpda
if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController
# pylint: disable=invalid-name
T = TypeVar("T", V1Pod, V1Deployment)
@ -56,6 +55,7 @@ class KubernetesObjectReconciler(Generic[T]):
}
).lower()
# pylint: disable=invalid-name
def up(self):
"""Create object if it doesn't exist, update if needed or recreate if needed."""
current = None

View File

@ -0,0 +1,28 @@
# Generated by Django 4.1.7 on 2023-03-07 13:41
from django.db import migrations, models
from authentik.lib.migrations import fallback_names
class Migration(migrations.Migration):
dependencies = [
("authentik_outposts", "0018_kubernetesserviceconnection_verify_ssl"),
]
operations = [
migrations.RunPython(fallback_names("authentik_outposts", "outpost", "name")),
migrations.RunPython(
fallback_names("authentik_outposts", "outpostserviceconnection", "name")
),
migrations.AlterField(
model_name="outpost",
name="name",
field=models.TextField(unique=True),
),
migrations.AlterField(
model_name="outpostserviceconnection",
name="name",
field=models.TextField(unique=True),
),
]

View File

@ -113,7 +113,7 @@ class OutpostServiceConnection(models.Model):
"""Connection details for an Outpost Controller, like Docker or Kubernetes"""
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
name = models.TextField()
name = models.TextField(unique=True)
local = models.BooleanField(
default=False,
@ -239,7 +239,7 @@ class Outpost(SerializerModel, ManagedModel):
"""Outpost instance which manages a service user and token"""
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
name = models.TextField()
name = models.TextField(unique=True)
type = models.TextField(choices=OutpostType.choices, default=OutpostType.PROXY)
service_connection = InheritanceForeignKey(

View File

@ -7,7 +7,6 @@ from urllib.parse import urlparse
import yaml
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.core.cache import cache
from django.db import DatabaseError, InternalError, ProgrammingError
from django.db.models.base import Model
@ -43,6 +42,7 @@ from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesContro
from authentik.providers.proxy.controllers.docker import ProxyDockerController
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
from authentik.root.celery import CELERY_APP
from authentik.root.messages.storage import closing_send
LOGGER = get_logger()
CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s"
@ -217,26 +217,23 @@ def outpost_post_save(model_class: str, model_pk: Any):
def outpost_send_update(model_instace: Model):
"""Send outpost update to all registered outposts, regardless to which authentik
instance they are connected"""
channel_layer = get_channel_layer()
if isinstance(model_instace, OutpostModel):
for outpost in model_instace.outpost_set.all():
_outpost_single_update(outpost, channel_layer)
_outpost_single_update(outpost)
elif isinstance(model_instace, Outpost):
_outpost_single_update(model_instace, channel_layer)
_outpost_single_update(model_instace)
def _outpost_single_update(outpost: Outpost, layer=None):
def _outpost_single_update(outpost: Outpost):
"""Update outpost instances connected to a single outpost"""
# Ensure token again, because this function is called when anything related to an
# OutpostModel is saved, so we can be sure permissions are right
_ = outpost.token
outpost.build_user_permissions(outpost.user)
if not layer: # pragma: no cover
layer = get_channel_layer()
for state in OutpostState.for_outpost(outpost):
for channel in state.channel_ids:
LOGGER.debug("sending update", channel=channel, instance=state.uid, outpost=outpost)
async_to_sync(layer.send)(channel, {"type": "event.update"})
async_to_sync(closing_send)(channel, {"type": "event.update"})
@CELERY_APP.task()

View File

@ -4,6 +4,7 @@ from rest_framework.test import APITestCase
from authentik.core.models import PropertyMapping
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.lib.generators import generate_id
from authentik.outposts.api.outposts import OutpostSerializer
from authentik.outposts.models import OutpostType, default_outpost_config
from authentik.providers.ldap.models import LDAPProvider
@ -16,7 +17,7 @@ class TestOutpostServiceConnectionsAPI(APITestCase):
def setUp(self) -> None:
super().setUp()
self.mapping = PropertyMapping.objects.create(
name="dummy", expression="""return {'foo': 'bar'}"""
name=generate_id(), expression="""return {'foo': 'bar'}"""
)
self.user = create_test_admin_user()
self.client.force_login(self.user)
@ -25,12 +26,12 @@ class TestOutpostServiceConnectionsAPI(APITestCase):
"""Test Outpost validation"""
valid = OutpostSerializer(
data={
"name": "foo",
"name": generate_id(),
"type": OutpostType.PROXY,
"config": default_outpost_config(),
"providers": [
ProxyProvider.objects.create(
name="test", authorization_flow=create_test_flow()
name=generate_id(), authorization_flow=create_test_flow()
).pk
],
}
@ -38,12 +39,12 @@ class TestOutpostServiceConnectionsAPI(APITestCase):
self.assertTrue(valid.is_valid())
invalid = OutpostSerializer(
data={
"name": "foo",
"name": generate_id(),
"type": OutpostType.PROXY,
"config": default_outpost_config(),
"providers": [
LDAPProvider.objects.create(
name="test", authorization_flow=create_test_flow()
name=generate_id(), authorization_flow=create_test_flow()
).pk
],
}
@ -60,15 +61,19 @@ class TestOutpostServiceConnectionsAPI(APITestCase):
def test_outpost_config(self):
"""Test Outpost's config field"""
provider = ProxyProvider.objects.create(name="test", authorization_flow=create_test_flow())
invalid = OutpostSerializer(data={"name": "foo", "providers": [provider.pk], "config": ""})
provider = ProxyProvider.objects.create(
name=generate_id(), authorization_flow=create_test_flow()
)
invalid = OutpostSerializer(
data={"name": generate_id(), "providers": [provider.pk], "config": ""}
)
self.assertFalse(invalid.is_valid())
self.assertIn("config", invalid.errors)
valid = OutpostSerializer(
data={
"name": "foo",
"name": generate_id(),
"providers": [provider.pk],
"config": default_outpost_config("foo"),
"config": default_outpost_config(generate_id()),
"type": OutpostType.PROXY,
}
)

View File

@ -7,11 +7,6 @@ GAUGE_POLICIES_CACHED = Gauge(
"authentik_policies_cached",
"Cached Policies",
)
HIST_POLICIES_BUILD_TIME = Histogram(
"authentik_policies_build_time",
"Execution times complete policy result to an object",
["object_pk", "object_type"],
)
HIST_POLICIES_EXECUTION_TIME = Histogram(
"authentik_policies_execution_time",

View File

@ -10,7 +10,6 @@ from sentry_sdk.tracing import Span
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.models import User
from authentik.policies.apps import HIST_POLICIES_BUILD_TIME
from authentik.policies.exceptions import PolicyEngineException
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel, PolicyEngineMode
from authentik.policies.process import PolicyProcess, cache_key
@ -86,10 +85,6 @@ class PolicyEngine:
op="authentik.policy.engine.build",
description=self.__pbm,
) as span,
HIST_POLICIES_BUILD_TIME.labels(
object_pk=str(self.__pbm.pk),
object_type=f"{self.__pbm._meta.app_label}.{self.__pbm._meta.model_name}",
).time(),
):
span: Span
span.set_data("pbm", self.__pbm)

View File

@ -0,0 +1,20 @@
# Generated by Django 4.1.7 on 2023-03-07 13:41
from django.db import migrations, models
from authentik.lib.migrations import fallback_names
class Migration(migrations.Migration):
dependencies = [
("authentik_policies", "0009_alter_policy_name"),
]
operations = [
migrations.RunPython(fallback_names("authentik_policies", "policy", "name")),
migrations.AlterField(
model_name="policy",
name="name",
field=models.TextField(unique=True),
),
]

View File

@ -158,7 +158,7 @@ class Policy(SerializerModel, CreatedUpdatedModel):
policy_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
name = models.TextField()
name = models.TextField(unique=True)
execution_logging = models.BooleanField(
default=False,

View File

@ -2,7 +2,8 @@
from django.core.cache import cache
from django.test import TestCase
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.engine import PolicyEngine
from authentik.policies.exceptions import PolicyEngineException
@ -17,11 +18,17 @@ class TestPolicyEngine(TestCase):
def setUp(self):
clear_policy_cache()
self.user = User.objects.create_user(username="policyuser")
self.policy_false = DummyPolicy.objects.create(result=False, wait_min=0, wait_max=1)
self.policy_true = DummyPolicy.objects.create(result=True, wait_min=0, wait_max=1)
self.policy_wrong_type = Policy.objects.create(name="wrong_type")
self.policy_raises = ExpressionPolicy.objects.create(name="raises", expression="{{ 0/0 }}")
self.user = create_test_admin_user()
self.policy_false = DummyPolicy.objects.create(
name=generate_id(), result=False, wait_min=0, wait_max=1
)
self.policy_true = DummyPolicy.objects.create(
name=generate_id(), result=True, wait_min=0, wait_max=1
)
self.policy_wrong_type = Policy.objects.create(name=generate_id())
self.policy_raises = ExpressionPolicy.objects.create(
name=generate_id(), expression="{{ 0/0 }}"
)
def test_engine_empty(self):
"""Ensure empty policy list passes"""

View File

@ -1,5 +1,6 @@
"""OAuth2Provider API Views"""
from django.urls import reverse
from django.utils import timezone
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.fields import CharField
@ -153,6 +154,7 @@ class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
user=request.user,
provider=provider,
_scope=" ".join(scope_names),
auth_time=timezone.now(),
),
request,
)

View File

@ -141,16 +141,21 @@ class AuthorizeError(OAuth2Error):
),
}
# pylint: disable=too-many-arguments
def __init__(
self,
redirect_uri: str,
error: str,
grant_type: str,
state: str,
description: Optional[str] = None,
):
super().__init__()
self.error = error
self.description = self.errors[error]
if description:
self.description = description
else:
self.description = self.errors[error]
self.redirect_uri = redirect_uri
self.grant_type = grant_type
self.state = state
@ -169,10 +174,12 @@ class AuthorizeError(OAuth2Error):
# See:
# http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError
hash_or_question = "#" if self.grant_type == GrantTypes.IMPLICIT else "?"
fragment_or_query = (
"#" if self.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID] else "?"
)
uri = (
f"{self.redirect_uri}{hash_or_question}error="
f"{self.redirect_uri}{fragment_or_query}error="
f"{self.error}&error_description={description}"
)

View File

@ -110,12 +110,11 @@ class IDToken:
# Convert datetimes into timestamps.
now = timezone.now()
id_token.iat = int(now.timestamp())
id_token.auth_time = int(token.auth_time.timestamp())
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
auth_event = get_login_event(request)
if auth_event:
auth_time = auth_event.created
id_token.auth_time = int(auth_time.timestamp())
# Also check which method was used for authentication
method = auth_event.context.get(PLAN_CONTEXT_METHOD, "")
method_args = auth_event.context.get(PLAN_CONTEXT_METHOD_ARGS, {})

View File

@ -0,0 +1,40 @@
# Generated by Django 4.1.7 on 2023-02-22 22:23
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_oauth2", "0014_alter_refreshtoken_options_and_more"),
]
operations = [
migrations.AddField(
model_name="accesstoken",
name="auth_time",
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name="Authentication time",
),
preserve_default=False,
),
migrations.AddField(
model_name="authorizationcode",
name="auth_time",
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name="Authentication time",
),
preserve_default=False,
),
migrations.AddField(
model_name="refreshtoken",
name="auth_time",
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name="Authentication time",
),
preserve_default=False,
),
]

View File

@ -226,7 +226,7 @@ class OAuth2Provider(Provider):
def get_issuer(self, request: HttpRequest) -> Optional[str]:
"""Get issuer, based on request"""
if self.issuer_mode == IssuerMode.GLOBAL:
return request.build_absolute_uri("/")
return request.build_absolute_uri(reverse("authentik_core:root-redirect"))
try:
url = reverse(
"authentik_providers_oauth2:provider-root",
@ -282,6 +282,7 @@ class BaseGrantModel(models.Model):
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
revoked = models.BooleanField(default=False)
_scope = models.TextField(default="", verbose_name=_("Scopes"))
auth_time = models.DateTimeField(verbose_name="Authentication time")
@property
def scope(self) -> list[str]:

View File

@ -204,6 +204,7 @@ class TestAuthorize(OAuthTestCase):
"redirect_uri": "http://local.invalid/Foo",
"scope": "openid",
"state": "foo",
"nonce": generate_id(),
},
)
self.assertEqual(
@ -325,6 +326,7 @@ class TestAuthorize(OAuthTestCase):
"state": state,
"scope": "openid",
"redirect_uri": "http://localhost",
"nonce": generate_id(),
},
)
response = self.client.get(
@ -378,6 +380,7 @@ class TestAuthorize(OAuthTestCase):
"state": state,
"scope": "openid",
"redirect_uri": "http://localhost",
"nonce": generate_id(),
},
)
response = self.client.get(

View File

@ -4,6 +4,7 @@ from base64 import b64encode
from dataclasses import asdict
from django.urls import reverse
from django.utils import timezone
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
@ -41,6 +42,7 @@ class TesOAuth2Introspection(OAuthTestCase):
provider=self.provider,
user=self.user,
token=generate_id(),
auth_time=timezone.now(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(
@ -72,6 +74,7 @@ class TesOAuth2Introspection(OAuthTestCase):
provider=self.provider,
user=self.user,
token=generate_id(),
auth_time=timezone.now(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(

View File

@ -4,6 +4,7 @@ from base64 import b64encode
from dataclasses import asdict
from django.urls import reverse
from django.utils import timezone
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
@ -40,6 +41,7 @@ class TesOAuth2Revoke(OAuthTestCase):
provider=self.provider,
user=self.user,
token=generate_id(),
auth_time=timezone.now(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(
@ -62,6 +64,7 @@ class TesOAuth2Revoke(OAuthTestCase):
provider=self.provider,
user=self.user,
token=generate_id(),
auth_time=timezone.now(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(

View File

@ -4,6 +4,7 @@ from json import dumps
from django.test import RequestFactory
from django.urls import reverse
from django.utils import timezone
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
@ -45,7 +46,9 @@ class TestToken(OAuthTestCase):
)
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
user = create_test_admin_user()
code = AuthorizationCode.objects.create(code="foobar", provider=provider, user=user)
code = AuthorizationCode.objects.create(
code="foobar", provider=provider, user=user, auth_time=timezone.now()
)
request = self.factory.post(
"/",
data={
@ -99,6 +102,7 @@ class TestToken(OAuthTestCase):
provider=provider,
user=user,
token=generate_id(),
auth_time=timezone.now(),
)
request = self.factory.post(
"/",
@ -127,7 +131,9 @@ class TestToken(OAuthTestCase):
self.app.save()
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
user = create_test_admin_user()
code = AuthorizationCode.objects.create(code="foobar", provider=provider, user=user)
code = AuthorizationCode.objects.create(
code="foobar", provider=provider, user=user, auth_time=timezone.now()
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
data={
@ -173,6 +179,7 @@ class TestToken(OAuthTestCase):
user=user,
token=generate_id(),
_id_token=dumps({}),
auth_time=timezone.now(),
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
@ -221,6 +228,7 @@ class TestToken(OAuthTestCase):
user=user,
token=generate_id(),
_id_token=dumps({}),
auth_time=timezone.now(),
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
@ -271,6 +279,7 @@ class TestToken(OAuthTestCase):
user=user,
token=generate_id(),
_id_token=dumps({}),
auth_time=timezone.now(),
)
# Create initial refresh token
response = self.client.post(

View File

@ -3,6 +3,7 @@ import json
from dataclasses import asdict
from django.urls import reverse
from django.utils import timezone
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application
@ -37,6 +38,7 @@ class TestUserinfo(OAuthTestCase):
provider=self.provider,
user=self.user,
token=generate_id(),
auth_time=timezone.now(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(
@ -56,7 +58,6 @@ class TestUserinfo(OAuthTestCase):
{
"name": self.user.name,
"given_name": self.user.name,
"family_name": "",
"preferred_username": self.user.name,
"nickname": self.user.name,
"groups": [group.name for group in self.user.ak_groups.all()],
@ -79,7 +80,6 @@ class TestUserinfo(OAuthTestCase):
{
"name": self.user.name,
"given_name": self.user.name,
"family_name": "",
"preferred_username": self.user.name,
"nickname": self.user.name,
"groups": [group.name for group in self.user.ak_groups.all()],

View File

@ -42,7 +42,7 @@ urlpatterns = [
path("<slug:application_slug>/jwks/", JWKSView.as_view(), name="jwks"),
path(
"<slug:application_slug>/",
RedirectView.as_view(pattern_name="authentk_providers_oauth2:provider-info"),
RedirectView.as_view(pattern_name="authentik_providers_oauth2:provider-info"),
name="provider-root",
),
path(

View File

@ -146,9 +146,10 @@ def protected_resource_view(scopes: list[str]):
LOGGER.warning("Revoked token was used", access_token=access_token)
Event.new(
action=EventAction.SUSPICIOUS_REQUEST,
message="Revoked refresh token was used",
token=access_token,
).from_http(request)
message="Revoked access token was used",
token=token,
provider=token.provider,
).from_http(request, user=token.user)
raise BearerTokenError("invalid_token")
if not set(scopes).issubset(set(token.scope)):

View File

@ -17,13 +17,14 @@ from structlog.stdlib import get_logger
from authentik.core.models import Application
from authentik.events.models import Event, EventAction
from authentik.events.utils import get_user
from authentik.events.signals import get_login_event
from authentik.flows.challenge import (
PLAN_CONTEXT_TITLE,
AutosubmitChallenge,
ChallengeTypes,
HttpChallengeResponse,
)
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.stage import StageView
@ -64,12 +65,11 @@ from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_PERMISSIONS,
ConsentStageView,
)
from authentik.stages.user_login.stage import USER_LOGIN_AUTHENTICATED
LOGGER = get_logger()
PLAN_CONTEXT_PARAMS = "params"
SESSION_KEY_NEEDS_LOGIN = "authentik/providers/oauth2/needs_login"
SESSION_KEY_LAST_LOGIN_UID = "authentik/providers/oauth2/last_login_uid"
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN}
@ -158,13 +158,14 @@ class OAuthAuthorizationParams:
request=query_dict.get("request", None),
max_age=int(max_age) if max_age else None,
code_challenge=query_dict.get("code_challenge"),
code_challenge_method=query_dict.get("code_challenge_method"),
code_challenge_method=query_dict.get("code_challenge_method", "plain"),
)
def __post_init__(self):
try:
self.provider: OAuth2Provider = OAuth2Provider.objects.get(client_id=self.client_id)
except OAuth2Provider.DoesNotExist:
self.provider: OAuth2Provider = OAuth2Provider.objects.filter(
client_id=self.client_id
).first()
if not self.provider:
LOGGER.warning("Invalid client identifier", client_id=self.client_id)
raise ClientIdError(client_id=self.client_id)
self.check_redirect_uri()
@ -234,40 +235,54 @@ class OAuthAuthorizationParams:
def check_nonce(self):
"""Nonce parameter validation."""
# https://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation
# Nonce is only required for Implicit flows
if self.grant_type != GrantTypes.IMPLICIT:
# nonce is required for all flows that return an id_token from the authorization endpoint,
# see https://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthRequest or
# https://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken and
# https://bitbucket.org/openid/connect/issues/972/nonce-requirement-in-hybrid-auth-request
if self.response_type not in [
ResponseTypes.ID_TOKEN,
ResponseTypes.ID_TOKEN_TOKEN,
ResponseTypes.CODE_ID_TOKEN,
ResponseTypes.CODE_ID_TOKEN_TOKEN,
]:
return
if SCOPE_OPENID not in self.scope:
return
if not self.nonce:
self.nonce = self.state
LOGGER.warning("Using state as nonce for OpenID Request")
if not self.nonce:
if SCOPE_OPENID in self.scope:
LOGGER.warning("Missing nonce for OpenID Request")
raise AuthorizeError(
self.redirect_uri, "invalid_request", self.grant_type, self.state
)
LOGGER.warning("Missing nonce for OpenID Request")
raise AuthorizeError(self.redirect_uri, "invalid_request", self.grant_type, self.state)
def check_code_challenge(self):
"""PKCE validation of the transformation method."""
if self.code_challenge and self.code_challenge_method not in ["plain", "S256"]:
raise AuthorizeError(self.redirect_uri, "invalid_request", self.grant_type, self.state)
raise AuthorizeError(
self.redirect_uri,
"invalid_request",
self.grant_type,
self.state,
f"Unsupported challenge method {self.code_challenge_method}",
)
def create_code(self, request: HttpRequest) -> AuthorizationCode:
"""Create an AuthorizationCode object for the request"""
code = AuthorizationCode()
code.user = request.user
code.provider = self.provider
auth_event = get_login_event(request)
code.code = uuid4().hex
now = timezone.now()
code = AuthorizationCode(
user=request.user,
provider=self.provider,
auth_time=auth_event.created if auth_event else now,
code=uuid4().hex,
expires=now + timedelta_from_string(self.provider.access_code_validity),
scope=self.scope,
nonce=self.nonce,
)
if self.code_challenge and self.code_challenge_method:
code.code_challenge = self.code_challenge
code.code_challenge_method = self.code_challenge_method
code.expires = timezone.now() + timedelta_from_string(self.provider.access_code_validity)
code.scope = self.scope
code.nonce = self.nonce
return code
@ -302,7 +317,6 @@ class AuthorizationFlowInitView(PolicyAccessView):
self.params.grant_type,
self.params.state,
)
error.to_event(redirect_uri=error.redirect_uri).from_http(self.request)
raise RequestValidationError(error.get_response(self.request))
def resolve_provider_application(self):
@ -322,45 +336,60 @@ class AuthorizationFlowInitView(PolicyAccessView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Start FlowPLanner, return to flow executor shell"""
# Require a login event to be set, otherwise make the user re-login
login_event = get_login_event(request)
if not login_event:
LOGGER.warning("request with no login event")
return self.handle_no_permission()
login_uid = str(login_event.pk)
# After we've checked permissions, and the user has access, check if we need
# to re-authenticate the user
if self.params.max_age:
current_age: timedelta = (
timezone.now()
- Event.objects.filter(action=EventAction.LOGIN, user=get_user(self.request.user))
.latest("created")
.created
)
# Attempt to check via the session's login event if set, otherwise we can't
# check
login_time = login_event.created
current_age: timedelta = timezone.now() - login_time
if current_age.total_seconds() > self.params.max_age:
LOGGER.debug(
"Triggering authentication as max_age requirement",
max_age=self.params.max_age,
ago=int(current_age.total_seconds()),
)
# Since we already need to re-authenticate the user, set the old login UID
# in case this request has both max_age and prompt=login
self.request.session[SESSION_KEY_LAST_LOGIN_UID] = login_uid
return self.handle_no_permission()
# If prompt=login, we need to re-authenticate the user regardless
if (
PROMPT_LOGIN in self.params.prompt
and SESSION_KEY_NEEDS_LOGIN not in self.request.session
# To prevent the user from having to double login when prompt is set to login
# and the user has just signed it. This session variable is set in the UserLoginStage
# and is (quite hackily) removed from the session in applications's API's List method
and USER_LOGIN_AUTHENTICATED not in self.request.session
):
self.request.session[SESSION_KEY_NEEDS_LOGIN] = True
return self.handle_no_permission()
# Check if we're not already doing the re-authentication
if PROMPT_LOGIN in self.params.prompt:
# No previous login UID saved, so save the current uid and trigger
# re-login, or previous login UID matches current one, so no re-login happened yet
if (
SESSION_KEY_LAST_LOGIN_UID not in self.request.session
or login_uid == self.request.session[SESSION_KEY_LAST_LOGIN_UID]
):
self.request.session[SESSION_KEY_LAST_LOGIN_UID] = login_uid
return self.handle_no_permission()
scope_descriptions = UserInfoView().get_scope_descriptions(self.params.scope)
# Regardless, we start the planner and return to it
planner = FlowPlanner(self.provider.authorization_flow)
planner.allow_empty_flows = True
plan = planner.plan(
self.request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: self.application,
# OAuth2 related params
PLAN_CONTEXT_PARAMS: self.params,
# Consent related params
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": self.application.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
},
)
try:
plan = planner.plan(
self.request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: self.application,
# OAuth2 related params
PLAN_CONTEXT_PARAMS: self.params,
# Consent related params
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": self.application.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
},
)
except FlowNonApplicableException:
return self.handle_no_permission_authenticated()
# OpenID clients can specify a `prompt` parameter, and if its set to consent we
# need to inject a consent stage
if PROMPT_CONSENT in self.params.prompt:
@ -518,6 +547,7 @@ class OAuthFulfillmentStage(StageView):
def create_implicit_response(self, code: Optional[AuthorizationCode]) -> dict:
"""Create implicit response's URL Fragment dictionary"""
query_fragment = {}
auth_event = get_login_event(self.request)
now = timezone.now()
access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity)
@ -526,6 +556,7 @@ class OAuthFulfillmentStage(StageView):
scope=self.params.scope,
expires=access_token_expiry,
provider=self.provider,
auth_time=auth_event.created if auth_event else now,
)
id_token = IDToken.new(self.provider, token, self.request)
@ -546,6 +577,8 @@ class OAuthFulfillmentStage(StageView):
ResponseTypes.CODE_TOKEN,
]:
query_fragment["access_token"] = token.token
# Get at_hash of the current token and update the id_token
id_token.at_hash = token.at_hash
# Check if response_type must include id_token in the response.
if self.params.response_type in [
@ -554,8 +587,6 @@ class OAuthFulfillmentStage(StageView):
ResponseTypes.CODE_ID_TOKEN,
ResponseTypes.CODE_ID_TOKEN_TOKEN,
]:
# Get at_hash of the current token and update the id_token
id_token.at_hash = token.at_hash
query_fragment["id_token"] = self.provider.encode(id_token.to_dict())
token._id_token = dumps(id_token.to_dict())

View File

@ -10,6 +10,7 @@ from structlog.stdlib import get_logger
from authentik.core.models import Application
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.stage import ChallengeStageView
@ -57,19 +58,23 @@ def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]:
scope_descriptions = UserInfoView().get_scope_descriptions(token.scope)
planner = FlowPlanner(token.provider.authorization_flow)
planner.allow_empty_flows = True
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: app,
# OAuth2 related params
PLAN_CONTEXT_DEVICE: token,
# Consent related params
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": app.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
},
)
try:
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: app,
# OAuth2 related params
PLAN_CONTEXT_DEVICE: token,
# Consent related params
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": app.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
},
)
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
return None
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
@ -97,7 +102,11 @@ class DeviceEntryView(View):
# Regardless, we start the planner and return to it
planner = FlowPlanner(device_flow)
planner.allow_empty_flows = True
plan = planner.plan(self.request)
try:
plan = planner.plan(self.request)
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
return HttpResponse(status=404)
plan.append_stage(in_memory_stage(OAuthDeviceCodeStage))
self.request.session[SESSION_KEY_PLAN] = plan

View File

@ -26,6 +26,7 @@ from authentik.core.models import (
User,
)
from authentik.events.models import Event, EventAction
from authentik.events.signals import get_login_event
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.engine import PolicyEngine
@ -262,8 +263,9 @@ class TokenParams:
Event.new(
action=EventAction.SUSPICIOUS_REQUEST,
message="Revoked refresh token was used",
token=raw_token,
).from_http(request)
token=self.refresh_token,
provider=self.refresh_token.provider,
).from_http(request, user=self.refresh_token.user)
raise TokenError("invalid_grant")
def __post_init_client_credentials(self, request: HttpRequest):
@ -478,6 +480,7 @@ class TokenView(View):
expires=access_token_expiry,
# Keep same scopes as previous token
scope=self.params.authorization_code.scope,
auth_time=self.params.authorization_code.auth_time,
)
access_token.id_token = IDToken.new(
self.provider,
@ -492,6 +495,7 @@ class TokenView(View):
scope=self.params.authorization_code.scope,
expires=refresh_token_expiry,
provider=self.provider,
auth_time=self.params.authorization_code.auth_time,
)
id_token = IDToken.new(
self.provider,
@ -520,7 +524,6 @@ class TokenView(View):
unauthorized_scopes = set(self.params.scope) - set(self.params.refresh_token.scope)
if unauthorized_scopes:
raise TokenError("invalid_scope")
now = timezone.now()
access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity)
access_token = AccessToken(
@ -529,6 +532,7 @@ class TokenView(View):
expires=access_token_expiry,
# Keep same scopes as previous token
scope=self.params.refresh_token.scope,
auth_time=self.params.refresh_token.auth_time,
)
access_token.id_token = IDToken.new(
self.provider,
@ -543,6 +547,7 @@ class TokenView(View):
scope=self.params.refresh_token.scope,
expires=refresh_token_expiry,
provider=self.provider,
auth_time=self.params.refresh_token.auth_time,
)
id_token = IDToken.new(
self.provider,
@ -577,6 +582,7 @@ class TokenView(View):
user=self.params.user,
expires=access_token_expiry,
scope=self.params.scope,
auth_time=now,
)
access_token.id_token = IDToken.new(
self.provider,
@ -599,11 +605,13 @@ class TokenView(View):
raise DeviceCodeError("authorization_pending")
now = timezone.now()
access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity)
auth_event = get_login_event(self.request)
access_token = AccessToken(
provider=self.provider,
user=self.params.device_code.user,
expires=access_token_expiry,
scope=self.params.device_code.scope,
auth_time=auth_event.created if auth_event else now,
)
access_token.id_token = IDToken.new(
self.provider,
@ -618,6 +626,7 @@ class TokenView(View):
scope=self.params.device_code.scope,
expires=refresh_token_expiry,
provider=self.provider,
auth_time=auth_event.created if auth_event else now,
)
id_token = IDToken.new(
self.provider,

View File

@ -73,9 +73,9 @@ class AssertionProcessor:
# https://commons.lbl.gov/display/IDMgmt/Attribute+Definitions
attribute_statement = Element(f"{{{NS_SAML_ASSERTION}}}AttributeStatement")
user = self.http_request.user
for mapping in self.provider.property_mappings.all().select_subclasses():
if not isinstance(mapping, SAMLPropertyMapping):
continue
for mapping in SAMLPropertyMapping.objects.filter(provider=self.provider).order_by(
"saml_name"
):
try:
mapping: SAMLPropertyMapping
value = mapping.evaluate(

View File

@ -1,6 +0,0 @@
"""saml provider settings"""
AUTHENTIK_PROVIDERS_SAML_PROCESSORS = [
"authentik.providers.saml.processors.generic",
"authentik.providers.saml.processors.salesforce",
]

View File

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

View File

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

View File

@ -1,7 +1,7 @@
"""authentik SAML IDP Views"""
from typing import Optional
from django.http import HttpRequest, HttpResponse
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
@ -11,6 +11,7 @@ from structlog.stdlib import get_logger
from authentik.core.models import Application
from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST
@ -60,16 +61,19 @@ class SAMLSSOView(PolicyAccessView):
# Regardless, we start the planner and return to it
planner = FlowPlanner(self.provider.authorization_flow)
planner.allow_empty_flows = True
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: self.application,
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": self.application.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
},
)
try:
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: self.application,
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": self.application.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
},
)
except FlowNonApplicableException:
raise Http404
plan.append_stage(in_memory_stage(SAMLFlowFinalView))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(

View File

View File

View File

@ -0,0 +1,38 @@
"""scim Property mappings API Views"""
from django_filters.filters import AllValuesMultipleFilter
from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.providers.scim.models import SCIMMapping
class SCIMMappingSerializer(PropertyMappingSerializer):
"""SCIMMapping Serializer"""
class Meta:
model = SCIMMapping
fields = PropertyMappingSerializer.Meta.fields
class SCIMMappingFilter(FilterSet):
"""Filter for SCIMMapping"""
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
class Meta:
model = SCIMMapping
fields = "__all__"
class SCIMMappingViewSet(UsedByMixin, ModelViewSet):
"""SCIMMapping Viewset"""
queryset = SCIMMapping.objects.all()
serializer_class = SCIMMappingSerializer
filterset_class = SCIMMappingFilter
search_fields = ["name"]
ordering = ["name"]

View File

@ -0,0 +1,62 @@
"""SCIM Provider API Views"""
from django.utils.text import slugify
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.tasks import TaskSerializer
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.events.monitored_tasks import TaskInfo
from authentik.providers.scim.models import SCIMProvider
class SCIMProviderSerializer(ProviderSerializer):
"""SCIMProvider Serializer"""
class Meta:
model = SCIMProvider
fields = [
"pk",
"name",
"property_mappings",
"property_mappings_group",
"component",
"assigned_application_slug",
"assigned_application_name",
"verbose_name",
"verbose_name_plural",
"meta_model_name",
"url",
"token",
"exclude_users_service_account",
"filter_group",
]
extra_kwargs = {}
class SCIMProviderViewSet(UsedByMixin, ModelViewSet):
"""SCIMProvider Viewset"""
queryset = SCIMProvider.objects.all()
serializer_class = SCIMProviderSerializer
filterset_fields = ["name", "exclude_users_service_account", "url", "filter_group"]
search_fields = ["name", "url"]
ordering = ["name", "url"]
@extend_schema(
responses={
200: TaskSerializer(),
404: OpenApiResponse(description="Task not found"),
}
)
@action(methods=["GET"], detail=True, pagination_class=None, filter_backends=[])
def sync_status(self, request: Request, pk: int) -> Response:
"""Get provider's sync status"""
provider = self.get_object()
task = TaskInfo.by_name(f"scim_sync:{slugify(provider.name)}")
if not task:
return Response(status=404)
return Response(TaskSerializer(task).data)

View File

@ -0,0 +1,15 @@
"""authentik SCIM Provider app config"""
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikProviderSCIMConfig(ManagedAppConfig):
"""authentik SCIM Provider app config"""
name = "authentik.providers.scim"
label = "authentik_providers_scim"
verbose_name = "authentik Providers.SCIM"
default = True
def reconcile_load_signals(self):
"""Load signals"""
self.import_module("authentik.providers.scim.signals")

View File

@ -0,0 +1,2 @@
"""SCIM constants"""
PAGE_SIZE = 100

View File

@ -0,0 +1,105 @@
"""SCIM Client"""
from typing import Generic, TypeVar
from pydantic import ValidationError
from pydanticscim.service_provider import (
Bulk,
ChangePassword,
Filter,
Patch,
ServiceProviderConfiguration,
Sort,
)
from requests import RequestException, Session
from structlog.stdlib import get_logger
from authentik.lib.utils.http import get_http_session
from authentik.providers.scim.clients.exceptions import SCIMRequestException
from authentik.providers.scim.models import SCIMProvider
T = TypeVar("T")
# pylint: disable=invalid-name
SchemaType = TypeVar("SchemaType")
def default_service_provider_config() -> ServiceProviderConfiguration:
"""Fallback service provider configuration"""
return ServiceProviderConfiguration(
patch=Patch(supported=False),
bulk=Bulk(supported=False),
filter=Filter(supported=False),
changePassword=ChangePassword(supported=False),
sort=Sort(supported=False),
authenticationSchemes=[],
)
class SCIMClient(Generic[T, SchemaType]):
"""SCIM Client"""
base_url: str
token: str
provider: SCIMProvider
_session: Session
_config: ServiceProviderConfiguration
def __init__(self, provider: SCIMProvider):
self._session = get_http_session()
self.provider = provider
# Remove trailing slashes as we assume the URL doesn't have any
base_url = provider.url
if base_url.endswith("/"):
base_url = base_url[:-1]
self.base_url = base_url
self.token = provider.token
self.logger = get_logger().bind(provider=provider.name)
self._config = self.get_service_provider_config()
def _request(self, method: str, path: str, **kwargs) -> dict:
"""Wrapper to send a request to the full URL"""
try:
response = self._session.request(
method,
f"{self.base_url}{path}",
**kwargs,
headers={
"Authorization": f"Bearer {self.token}",
"Accept": "application/scim+json",
"Content-Type": "application/scim+json",
},
)
except RequestException as exc:
raise SCIMRequestException(None) from exc
self.logger.debug("scim request", path=path, method=method, **kwargs)
if response.status_code >= 400:
self.logger.warning(
"Failed to send SCIM request", path=path, method=method, response=response.text
)
raise SCIMRequestException(response)
if response.status_code == 204:
return {}
return response.json()
def get_service_provider_config(self):
"""Get Service provider config"""
default_config = default_service_provider_config()
try:
return ServiceProviderConfiguration.parse_obj(
self._request("GET", "/ServiceProviderConfig")
)
except (ValidationError, SCIMRequestException) as exc:
self.logger.warning("failed to get ServiceProviderConfig", exc=exc)
return default_config
def write(self, obj: T):
"""Write object to SCIM"""
raise NotImplementedError()
def delete(self, obj: T):
"""Delete object from SCIM"""
raise NotImplementedError()
def to_scim(self, obj: T) -> SchemaType:
"""Convert object to scim"""
raise NotImplementedError()

View File

@ -0,0 +1,43 @@
"""SCIM Client exceptions"""
from typing import Optional
from pydantic import ValidationError
from pydanticscim.responses import SCIMError
from requests import Response
from authentik.lib.sentry import SentryIgnoredException
class StopSync(SentryIgnoredException):
"""Exception raised when a configuration error should stop the sync process"""
def __init__(self, exc: Exception, obj: object, mapping: Optional[object] = None) -> None:
self.exc = exc
self.obj = obj
self.mapping = mapping
def __str__(self) -> str:
msg = f"Error {str(self.exc)}, caused by {self.obj}"
if self.mapping:
msg += f" (mapping {self.mapping})"
return msg
class SCIMRequestException(SentryIgnoredException):
"""Exception raised when an SCIM request fails"""
_response: Optional[Response]
def __init__(self, response: Optional[Response] = None) -> None:
self._response = response
def __str__(self) -> str:
if not self._response:
return super().__str__()
try:
error = SCIMError.parse_raw(self._response.text)
return error.detail
except ValidationError:
pass
return super().__str__()

View File

@ -0,0 +1,167 @@
"""Group client"""
from deepmerge import always_merger
from pydantic import ValidationError
from pydanticscim.group import GroupMember
from pydanticscim.responses import PatchOp, PatchOperation, PatchRequest
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.models import Group
from authentik.events.models import Event, EventAction
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.utils import delete_none_keys
from authentik.providers.scim.clients.base import SCIMClient
from authentik.providers.scim.clients.exceptions import StopSync
from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema
from authentik.providers.scim.models import SCIMGroup, SCIMMapping, SCIMUser
class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
"""SCIM client for groups"""
def write(self, obj: Group):
"""Write a group"""
scim_group = SCIMGroup.objects.filter(provider=self.provider, group=obj).first()
if not scim_group:
return self._create(obj)
scim_group = self.to_scim(obj)
scim_group.id = scim_group.id
return self._request(
"PUT",
f"/Groups/{scim_group.id}",
data=scim_group.json(
exclude_unset=True,
),
)
def delete(self, obj: Group):
"""Delete group"""
scim_group = SCIMGroup.objects.filter(provider=self.provider, group=obj).first()
if not scim_group:
self.logger.debug("Group does not exist in SCIM, skipping")
return None
response = self._request("DELETE", f"/Groups/{scim_group.id}")
scim_group.delete()
return response
def to_scim(self, obj: Group) -> SCIMGroupSchema:
"""Convert authentik user into SCIM"""
raw_scim_group = {}
for mapping in (
self.provider.property_mappings_group.all().order_by("name").select_subclasses()
):
if not isinstance(mapping, SCIMMapping):
continue
try:
mapping: SCIMMapping
value = mapping.evaluate(
user=None,
request=None,
group=obj,
provider=self.provider,
)
if value is None:
continue
always_merger.merge(raw_scim_group, value)
except (PropertyMappingExpressionException, ValueError) as exc:
# Value error can be raised when assigning invalid data to an attribute
Event.new(
EventAction.CONFIGURATION_ERROR,
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
mapping=mapping,
).save()
raise StopSync(exc, obj, mapping) from exc
if not raw_scim_group:
raise StopSync(ValueError("No group mappings configured"), obj)
try:
scim_group = SCIMGroupSchema.parse_obj(delete_none_keys(raw_scim_group))
except ValidationError as exc:
raise StopSync(exc, obj) from exc
if not scim_group.externalId:
scim_group.externalId = str(obj.pk)
users = list(obj.users.order_by("id").values_list("id", flat=True))
connections = SCIMUser.objects.filter(provider=self.provider, user__pk__in=users)
for user in connections:
scim_group.members.append(
GroupMember(
value=user.id,
)
)
return scim_group
def _create(self, group: Group):
"""Create group from scratch and create a connection object"""
scim_group = self.to_scim(group)
response = self._request(
"POST",
"/Groups",
data=scim_group.json(
exclude_unset=True,
),
)
SCIMGroup.objects.create(provider=self.provider, group=group, id=response["id"])
def _patch(
self,
group_id: str,
*ops: PatchOperation,
):
req = PatchRequest(Operations=ops)
self._request("PATCH", f"/Groups/{group_id}", data=req.json(exclude_unset=True))
def update_group(self, group: Group, action: PatchOp, users_set: set[int]):
"""Update a group, either using PUT to replace it or PATCH if supported"""
if self._config.patch.supported:
if action == PatchOp.add:
return self._patch_add_users(group, users_set)
if action == PatchOp.remove:
return self._patch_remove_users(group, users_set)
return self.write(group)
def _patch_add_users(self, group: Group, users_set: set[int]):
"""Add users in users_set to group"""
if len(users_set) < 1:
return
scim_group = SCIMGroup.objects.filter(provider=self.provider, group=group).first()
if not scim_group:
self.logger.warning(
"could not sync group membership, group does not exist", group=group
)
return
user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"id", flat=True
)
)
self._patch(
scim_group.id,
PatchOperation(
op=PatchOp.add,
path="members",
value=[{"value": x} for x in user_ids],
),
)
def _patch_remove_users(self, group: Group, users_set: set[int]):
"""Remove users in users_set from group"""
if len(users_set) < 1:
return
scim_group = SCIMGroup.objects.filter(provider=self.provider, group=group).first()
if not scim_group:
self.logger.warning(
"could not sync group membership, group does not exist", group=group
)
return
user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"id", flat=True
)
)
self._patch(
scim_group.id,
PatchOperation(
op=PatchOp.remove,
path="members",
value=[{"value": x} for x in user_ids],
),
)

View File

@ -0,0 +1,17 @@
"""Custom SCIM schemas"""
from typing import Optional
from pydanticscim.group import Group as SCIMGroupSchema
from pydanticscim.user import User as SCIMUserSchema
class User(SCIMUserSchema):
"""Modified User schema with added externalId field"""
externalId: Optional[str] = None
class Group(SCIMGroupSchema):
"""Modified Group schema with added externalId field"""
externalId: Optional[str] = None

View File

@ -0,0 +1,92 @@
"""User client"""
from deepmerge import always_merger
from pydantic import ValidationError
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.models import User
from authentik.events.models import Event, EventAction
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.utils import delete_none_keys
from authentik.providers.scim.clients.base import SCIMClient
from authentik.providers.scim.clients.exceptions import StopSync
from authentik.providers.scim.clients.schema import User as SCIMUserSchema
from authentik.providers.scim.models import SCIMMapping, SCIMUser
class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
"""SCIM client for users"""
def write(self, obj: User):
"""Write a user"""
scim_user = SCIMUser.objects.filter(provider=self.provider, user=obj).first()
if not scim_user:
return self._create(obj)
return self._update(obj, scim_user)
def delete(self, obj: User):
"""Delete user"""
scim_user = SCIMUser.objects.filter(provider=self.provider, user=obj).first()
if not scim_user:
self.logger.debug("User does not exist in SCIM, skipping")
return None
response = self._request("DELETE", f"/Users/{scim_user.id}")
scim_user.delete()
return response
def to_scim(self, obj: User) -> SCIMUserSchema:
"""Convert authentik user into SCIM"""
raw_scim_user = {}
for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
if not isinstance(mapping, SCIMMapping):
continue
try:
mapping: SCIMMapping
value = mapping.evaluate(
user=obj,
request=None,
provider=self.provider,
)
if value is None:
continue
always_merger.merge(raw_scim_user, value)
except (PropertyMappingExpressionException, ValueError) as exc:
# Value error can be raised when assigning invalid data to an attribute
Event.new(
EventAction.CONFIGURATION_ERROR,
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
mapping=mapping,
).save()
raise StopSync(exc, obj, mapping) from exc
if not raw_scim_user:
raise StopSync(ValueError("No user mappings configured"), obj)
try:
scim_user = SCIMUserSchema.parse_obj(delete_none_keys(raw_scim_user))
except ValidationError as exc:
raise StopSync(exc, obj) from exc
if not scim_user.externalId:
scim_user.externalId = str(obj.uid)
return scim_user
def _create(self, user: User):
"""Create user from scratch and create a connection object"""
scim_user = self.to_scim(user)
response = self._request(
"POST",
"/Users",
data=scim_user.json(
exclude_unset=True,
),
)
SCIMUser.objects.create(provider=self.provider, user=user, id=response["id"])
def _update(self, user: User, connection: SCIMUser):
"""Update existing user"""
scim_user = self.to_scim(user)
scim_user.id = connection.id
self._request(
"PUT",
f"/Users/{connection.id}",
data=scim_user.json(
exclude_unset=True,
),
)

View File

@ -0,0 +1,62 @@
# Generated by Django 4.1.7 on 2023-03-02 13:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_core", "0024_source_icon"),
]
operations = [
migrations.CreateModel(
name="SCIMMapping",
fields=[
(
"propertymapping_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.propertymapping",
),
),
],
options={
"verbose_name": "SCIM Mapping",
"verbose_name_plural": "SCIM Mappings",
},
bases=("authentik_core.propertymapping",),
),
migrations.CreateModel(
name="SCIMProvider",
fields=[
(
"provider_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.provider",
),
),
(
"url",
models.TextField(help_text="Base URL to SCIM requests, usually ends in /v2"),
),
("token", models.TextField(help_text="Authentication token")),
],
options={
"verbose_name": "SCIM Provider",
"verbose_name_plural": "SCIM Providers",
},
bases=("authentik_core.provider",),
),
]

View File

@ -0,0 +1,137 @@
# Generated by Django 4.1.7 on 2023-03-07 13:07
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [
("authentik_providers_scim", "0001_initial"),
("authentik_providers_scim", "0002_scimuser"),
("authentik_providers_scim", "0003_scimgroup"),
("authentik_providers_scim", "0004_scimprovider_property_mappings_group"),
("authentik_providers_scim", "0005_scimprovider_exclude_users_service_account_and_more"),
("authentik_providers_scim", "0006_rename_parent_group_scimprovider_filter_group"),
]
initial = True
dependencies = [
("authentik_core", "0024_source_icon"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("authentik_core", "0025_alter_provider_authorization_flow"),
]
operations = [
migrations.CreateModel(
name="SCIMMapping",
fields=[
(
"propertymapping_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.propertymapping",
),
),
],
options={
"verbose_name": "SCIM Mapping",
"verbose_name_plural": "SCIM Mappings",
},
bases=("authentik_core.propertymapping",),
),
migrations.CreateModel(
name="SCIMProvider",
fields=[
(
"provider_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.provider",
),
),
(
"url",
models.TextField(help_text="Base URL to SCIM requests, usually ends in /v2"),
),
("token", models.TextField(help_text="Authentication token")),
(
"property_mappings_group",
models.ManyToManyField(
blank=True,
default=None,
help_text="Property mappings used for group creation/updating.",
to="authentik_core.propertymapping",
),
),
("exclude_users_service_account", models.BooleanField(default=False)),
(
"filter_group",
models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_core.group",
),
),
],
options={
"verbose_name": "SCIM Provider",
"verbose_name_plural": "SCIM Providers",
},
bases=("authentik_core.provider",),
),
migrations.CreateModel(
name="SCIMUser",
fields=[
("id", models.TextField(primary_key=True, serialize=False)),
(
"provider",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_providers_scim.scimprovider",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
"unique_together": {("id", "user", "provider")},
},
),
migrations.CreateModel(
name="SCIMGroup",
fields=[
("id", models.TextField(primary_key=True, serialize=False)),
(
"group",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.group"
),
),
(
"provider",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_providers_scim.scimprovider",
),
),
],
options={
"unique_together": {("id", "group", "provider")},
},
),
]

View File

@ -0,0 +1,37 @@
# Generated by Django 4.1.7 on 2023-03-02 15:03
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("authentik_providers_scim", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="SCIMUser",
fields=[
("id", models.TextField(primary_key=True, serialize=False)),
(
"provider",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_providers_scim.scimprovider",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
"unique_together": {("id", "user", "provider")},
},
),
]

View File

@ -0,0 +1,36 @@
# Generated by Django 4.1.7 on 2023-03-02 15:55
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0024_source_icon"),
("authentik_providers_scim", "0002_scimuser"),
]
operations = [
migrations.CreateModel(
name="SCIMGroup",
fields=[
("id", models.TextField(primary_key=True, serialize=False)),
(
"group",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.group"
),
),
(
"provider",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_providers_scim.scimprovider",
),
),
],
options={
"unique_together": {("id", "group", "provider")},
},
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.1.7 on 2023-03-03 14:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0025_alter_provider_authorization_flow"),
("authentik_providers_scim", "0003_scimgroup"),
]
operations = [
migrations.AddField(
model_name="scimprovider",
name="property_mappings_group",
field=models.ManyToManyField(
blank=True,
default=None,
help_text="Property mappings used for group creation/updating.",
to="authentik_core.propertymapping",
),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 4.1.7 on 2023-03-07 10:35
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0025_alter_provider_authorization_flow"),
("authentik_providers_scim", "0004_scimprovider_property_mappings_group"),
]
operations = [
migrations.AddField(
model_name="scimprovider",
name="exclude_users_service_account",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="scimprovider",
name="parent_group",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_core.group",
),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.1.7 on 2023-03-07 13:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_scim", "0005_scimprovider_exclude_users_service_account_and_more"),
]
operations = [
migrations.RenameField(
model_name="scimprovider",
old_name="parent_group",
new_name="filter_group",
),
]

View File

@ -0,0 +1,113 @@
"""SCIM Provider models"""
from django.db import models
from django.db.models import Q, QuerySet
from django.utils.translation import gettext_lazy as _
from guardian.shortcuts import get_anonymous_user
from rest_framework.serializers import Serializer
from authentik.core.models import USER_ATTRIBUTE_SA, Group, PropertyMapping, Provider, User
class SCIMProvider(Provider):
"""SCIM 2.0 provider to create users and groups in external applications"""
exclude_users_service_account = models.BooleanField(default=False)
filter_group = models.ForeignKey(
"authentik_core.group", on_delete=models.SET_DEFAULT, default=None, null=True
)
url = models.TextField(help_text=_("Base URL to SCIM requests, usually ends in /v2"))
token = models.TextField(help_text=_("Authentication token"))
property_mappings_group = models.ManyToManyField(
PropertyMapping,
default=None,
blank=True,
help_text=_("Property mappings used for group creation/updating."),
)
def get_user_qs(self) -> QuerySet[User]:
"""Get queryset of all users with consistent ordering
according to the provider's settings"""
base = User.objects.all().exclude(pk=get_anonymous_user().pk)
if self.exclude_users_service_account:
base = base.filter(
Q(
**{
f"attributes__{USER_ATTRIBUTE_SA}__isnull": True,
}
)
| Q(
**{
f"attributes__{USER_ATTRIBUTE_SA}": False,
}
)
)
if self.filter_group:
base = base.filter(ak_groups__in=[self.filter_group])
return base.order_by("pk")
def get_group_qs(self) -> QuerySet[Group]:
"""Get queryset of all groups with consistent ordering"""
return Group.objects.all().order_by("pk")
@property
def component(self) -> str:
return "ak-provider-scim-form"
@property
def serializer(self) -> type[Serializer]:
from authentik.providers.scim.api.providers import SCIMProviderSerializer
return SCIMProviderSerializer
def __str__(self):
return f"SCIM Provider {self.name}"
class Meta:
verbose_name = _("SCIM Provider")
verbose_name_plural = _("SCIM Providers")
class SCIMMapping(PropertyMapping):
"""Map authentik data to outgoing SCIM requests"""
@property
def component(self) -> str:
return "ak-property-mapping-scim-form"
@property
def serializer(self) -> type[Serializer]:
from authentik.providers.scim.api.property_mapping import SCIMMappingSerializer
return SCIMMappingSerializer
def __str__(self):
return f"SCIM Mapping {self.name}"
class Meta:
verbose_name = _("SCIM Mapping")
verbose_name_plural = _("SCIM Mappings")
class SCIMUser(models.Model):
"""Mapping of a user and provider to a SCIM user ID"""
id = models.TextField(primary_key=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
class Meta:
unique_together = (("id", "user", "provider"),)
class SCIMGroup(models.Model):
"""Mapping of a group and provider to a SCIM user ID"""
id = models.TextField(primary_key=True)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
class Meta:
unique_together = (("id", "group", "provider"),)

View File

@ -0,0 +1,12 @@
"""SCIM task Settings"""
from celery.schedules import crontab
from authentik.lib.utils.time import fqdn_rand
CELERY_BEAT_SCHEDULE = {
"providers_scim_sync": {
"task": "authentik.providers.scim.tasks.scim_sync_all",
"schedule": crontab(minute=fqdn_rand("scim_sync_all"), hour="*"),
"options": {"queue": "authentik_scheduled"},
},
}

View File

@ -0,0 +1,41 @@
"""SCIM provider signals"""
from django.db.models import Model
from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import receiver
from pydanticscim.responses import PatchOp
from structlog.stdlib import get_logger
from authentik.core.models import Group, User
from authentik.lib.utils.reflection import class_to_path
from authentik.providers.scim.models import SCIMProvider
from authentik.providers.scim.tasks import scim_signal_direct, scim_signal_m2m, scim_sync
LOGGER = get_logger()
@receiver(post_save, sender=SCIMProvider)
def post_save_provider(sender: type[Model], instance, created: bool, **_):
"""Trigger sync when SCIM provider is saved"""
scim_sync.delay(instance.pk)
@receiver(post_save, sender=User)
@receiver(post_save, sender=Group)
def post_save_scim(sender: type[Model], instance: User | Group, created: bool, **_):
"""Post save handler"""
scim_signal_direct.delay(class_to_path(instance.__class__), instance.pk, PatchOp.add.value)
@receiver(pre_delete, sender=User)
@receiver(pre_delete, sender=Group)
def pre_delete_scim(sender: type[Model], instance: User | Group, **_):
"""Pre-delete handler"""
scim_signal_direct.delay(class_to_path(instance.__class__), instance.pk, PatchOp.remove.value)
@receiver(m2m_changed, sender=User.ak_groups.through)
def m2m_changed_scim(sender: type[Model], instance, action: str, pk_set: set, **kwargs):
"""Sync group membership"""
if action not in ["post_add", "post_remove"]:
return
scim_signal_m2m.delay(str(instance.pk), action, list(pk_set))

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