Compare commits

..

236 Commits

Author SHA1 Message Date
fb0a88f2cf providers/proxy: rework endpoints logic (#4993)
* providers/proxy: rework endpoints logic

again...this time with tests and better logic

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

* fix tests

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-18 18:55:30 +01:00
4d8d405e70 blueprints: allow setting of token key in blueprint context (#4995)
closes #4717

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-18 18:55:25 +01:00
1d5f399b61 web/admin: fix prompt field display (#4990)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-18 18:54:41 +01:00
bb575fcc10 web/elements: fix search select inconsistency (#4989)
* web/elements: fix search-select inconsistency

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

* web/common: fix config having to be json converted everywhere

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

* web/elements: refactor form without iron-form

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

* web/admin: fix misc

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/package-lock.json
2023-03-18 18:54:33 +01:00
13fd1afbb9 web/admin: fix inconsistent display of flows in selections (#4977)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-18 18:53:16 +01:00
f059b998cc release: 2023.3.1 2023-03-16 18:09:53 +01:00
3f48202dfe web/flows: fix authenticator selector in dark mode (#4974)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-16 16:57:15 +01:00
2a3ebb616b providers/oauth2: fix response for response_type code and response_mode fragment (#4975) 2023-03-16 16:57:09 +01:00
ceab1f732d providers/ldap: fix duplicate attributes (#4972)
closes #4971

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-16 12:14:57 +01:00
01d2cce9ca Merge branch 'main' into version-2023.3 2023-03-15 20:20:51 +01:00
fd9293e3e8 web/user: fix custom user interface background with dark theme (#4960)
closes #4947

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-15 18:43:01 +01:00
520de8d5b0 web/common: fix tab label color on dark theme (#4959)
closes #4936

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-15 18:42:36 +01:00
bbdb0df42e website/docs: capitalization and clarifications (#4948)
* capitalization and clarifications

* minor edits

* Update website/docs/installation/docker-compose.md

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

* Update website/docs/installation/docker-compose.md

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

* fix lint

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

---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Tana Berry <tanaberry@Tanas-MacBook-Pro-authentik.local>
Co-authored-by: Jens L. <jens.langhammer@beryju.org>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-03-15 11:19:03 -05:00
9310d4cdc0 *: fix mismatched task names for discovery, make output service connection task monitored (#4956)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-15 12:12:08 +01:00
86f9056d3f core: fix url validator (#4957)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-15 12:00:57 +01:00
5375637eda website/docs: Fix detail and improve latest changelog regarding SCIM (#4955)
* Fix detail and improve latest changelog regarding SCIM

I found the wording confusing ("sync from" vs. "sync into" as being used in the docs)

Signed-off-by: Thomas McWork <thomas.mc.work@posteo.de>

* fix lint

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

---------

Signed-off-by: Thomas McWork <thomas.mc.work@posteo.de>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-03-15 11:35:07 +01:00
109f06c3ae web: bump @babel/core from 7.21.0 to 7.21.3 in /web (#4953)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.21.0 to 7.21.3.
- [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.3/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/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-03-15 11:20:13 +01:00
a3744da3a5 core: bump goauthentik.io/api/v3 from 3.2023030.2 to 3.2023030.3 (#4954)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023030.2 to 3.2023030.3.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023030.2...v3.2023030.3)

---
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-15 11:19:45 +01:00
ff1feb653b website: bump webpack from 5.73.0 to 5.76.1 in /website (#4950)
Bumps [webpack](https://github.com/webpack/webpack) from 5.73.0 to 5.76.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.73.0...v5.76.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 00:21:24 +01:00
4a11d89a08 core: bump google.golang.org/protobuf from 1.29.0 to 1.29.1 (#4949)
Bumps [google.golang.org/protobuf](https://github.com/protocolbuffers/protobuf-go) from 1.29.0 to 1.29.1.
- [Release notes](https://github.com/protocolbuffers/protobuf-go/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf-go/blob/master/release.bash)
- [Commits](https://github.com/protocolbuffers/protobuf-go/compare/v1.29.0...v1.29.1)

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 00:11:28 +01:00
73d7b5f110 root: add common fixture loader (#4946)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-14 17:13:03 +01:00
8b7a92068b website/docs: forward-auth page, add list of links (#4937)
* add list of links

* added commas

* fix build

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Tana Berry <tanaberry@Tanas-MacBook-Pro-authentik.local>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-03-14 07:45:49 -05:00
ff1532da13 website/integrations: Changes to reverse proxy information for grafana (#4938)
Changes to reverse proxy information for grafana

Changed to remove the port at the end of the domain for root_url, if grafana is behind a reverse proxy and is reachable at its ip or at https://grafana.company it would not than be accessible by that port. 

Until the root_url was changed in grafana.ini to https://grafana.company/ gives the following error  The request fails due to a missing, invalid, or mismatching redirection URI (redirect_uri).

This was tested using:
authentik 2023.3.0
grafana 9.3.6
nginx proxy manager 2.9.19

Signed-off-by: SiskoUrso <91812199+SiskoUrso@users.noreply.github.com>
2023-03-14 13:44:08 +01:00
6eafa2346d core: bump goauthentik.io/api/v3 from 3.2023022.15 to 3.2023030.2 (#4942)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023022.15 to 3.2023030.2.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023022.15...v3.2023030.2)

---
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-03-14 11:31:59 +01:00
681644b854 web: bump @sentry/tracing from 7.42.0 to 7.43.0 in /web (#4939)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 7.42.0 to 7.43.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.42.0...7.43.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-14 11:31:45 +01:00
de4d388e0a web: bump @sentry/browser from 7.42.0 to 7.43.0 in /web (#4940)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 7.42.0 to 7.43.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.42.0...7.43.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-14 11:30:11 +01:00
cbe2cb51e7 web: bump @typescript-eslint/eslint-plugin from 5.54.1 to 5.55.0 in /web (#4941)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.54.1 to 5.55.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.55.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-03-14 11:29:46 +01:00
9176c71075 web: bump core-js from 3.29.0 to 3.29.1 in /web (#4944) 2023-03-14 10:29:47 +01:00
1c05e4ca09 web: bump @typescript-eslint/parser from 5.54.1 to 5.55.0 in /web (#4943) 2023-03-14 10:29:27 +01:00
2d55d3c743 web/admin: fix wizards with radio selects not working correctly after use (#4933)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-13 23:42:52 +01:00
0a9482b28a web: bump API Client version (#4934)
Signed-off-by: GitHub <noreply@github.com>
2023-03-13 23:38:58 +01:00
4b1440944e providers: fix authorization_flow not required in API (#4932)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-13 23:36:24 +01:00
75794defc6 website/docs: capitalization of product names (#4922)
Docker and Traefik: for product names we need to follow their brand. Exception is with command lines, etc that are often not capitalized.

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
2023-03-13 17:10:21 -05:00
59a92dbacd stages/authenticator_webauthn: remove credential_id size limit (#4931)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-13 21:24:10 +01:00
b81ddf2b80 web/flows: update background (#4927)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-13 19:14:04 +01:00
9ccd1ce08b web: bump API Client version (#4928)
Signed-off-by: GitHub <noreply@github.com>
2023-03-13 19:13:33 +01:00
6f6d22da13 release: 2023.3.0 (#4925) 2023-03-13 19:10:48 +01:00
095850f038 website/docs: add new release to sidebar, cleanup (#4926)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-13 19:04:25 +01:00
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
440 changed files with 13406 additions and 10604 deletions

View File

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

View File

@ -7,8 +7,11 @@ charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[html] [*.html]
indent_size = 2 indent_size = 2
[yaml] [*.{yaml,yml}]
indent_size = 2 indent_size = 2
[*.go]
indent_style = tab

View File

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

View File

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

3
.gitignore vendored
View File

@ -200,3 +200,6 @@ media/
.idea/ .idea/
/gen-*/ /gen-*/
data/ 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", "passwordless",
"kubernetes", "kubernetes",
"sso", "sso",
"slo" "slo",
"scim",
], ],
"python.linting.pylintEnabled": true, "python.linting.pylintEnabled": true,
"todo-tree.tree.showCountsInTree": 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 ## 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 ### Git Commit Messages
- Use the format of `<package>: <verb> <description>` - Use the format of `<package>: <verb> <description>`
- See [here](#authentik-packages) for `package` - See [here](#authentik-packages) for `package`
- Example: `providers/saml2: fix parsing of requests` - Example: `providers/saml2: fix parsing of requests`
- Reference issues and pull requests liberally after the first line - 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 ### 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 poetry export -f requirements.txt --dev --output requirements-dev.txt
# Stage 4: Build go proxy # 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 WORKDIR /work
@ -96,7 +96,7 @@ RUN apt-get update && \
COPY ./authentik/ /authentik COPY ./authentik/ /authentik
COPY ./pyproject.toml / COPY ./pyproject.toml /
COPY ./xml /xml COPY ./schemas /schemas
COPY ./locale /locale COPY ./locale /locale
COPY ./tests /tests COPY ./tests /tests
COPY ./manage.py / COPY ./manage.py /

View File

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

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
from typing import Optional from typing import Optional
__version__ = "2023.2.1" __version__ = "2023.3.1"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" 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.models import Group, User
from authentik.core.tasks import clean_expired_models from authentik.core.tasks import clean_expired_models
from authentik.events.monitored_tasks import TaskResultStatus from authentik.events.monitored_tasks import TaskResultStatus
from authentik.lib.generators import generate_id
class TestAdminAPI(TestCase): class TestAdminAPI(TestCase):
@ -16,8 +17,8 @@ class TestAdminAPI(TestCase):
def setUp(self) -> None: def setUp(self) -> None:
super().setUp() super().setUp()
self.user = User.objects.create(username="test-user") self.user = User.objects.create(username=generate_id())
self.group = Group.objects.create(name="superusers", is_superuser=True) self.group = Group.objects.create(name=generate_id(), is_superuser=True)
self.group.users.add(self.user) self.group.users.add(self.user)
self.group.save() self.group.save()
self.client.force_login(self.user) self.client.force_login(self.user)

View File

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

View File

@ -4,6 +4,7 @@ from guardian.shortcuts import assign_perm
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from authentik.core.models import Application, User from authentik.core.models import Application, User
from authentik.lib.generators import generate_id
class TestAPIDecorators(APITestCase): class TestAPIDecorators(APITestCase):
@ -16,7 +17,7 @@ class TestAPIDecorators(APITestCase):
def test_obj_perm_denied(self): def test_obj_perm_denied(self):
"""Test object perm denied""" """Test object perm denied"""
self.client.force_login(self.user) 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( response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug}) reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
) )
@ -25,7 +26,7 @@ class TestAPIDecorators(APITestCase):
def test_other_perm_denied(self): def test_other_perm_denied(self):
"""Test other perm denied""" """Test other perm denied"""
self.client.force_login(self.user) 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) assign_perm("authentik_core.view_application", self.user, app)
response = self.client.get( response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug}) 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.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet
from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet
from authentik.providers.saml.api.providers import SAMLProviderViewSet 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.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
from authentik.sources.oauth.api.source import OAuthSourceViewSet from authentik.sources.oauth.api.source import OAuthSourceViewSet
from authentik.sources.oauth.api.source_connection import UserOAuthSourceConnectionViewSet 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/proxy", ProxyProviderViewSet)
router.register("providers/oauth2", OAuth2ProviderViewSet) router.register("providers/oauth2", OAuth2ProviderViewSet)
router.register("providers/saml", SAMLProviderViewSet) router.register("providers/saml", SAMLProviderViewSet)
router.register("providers/scim", SCIMProviderViewSet)
router.register("oauth2/authorization_codes", AuthorizationCodeViewSet) router.register("oauth2/authorization_codes", AuthorizationCodeViewSet)
router.register("oauth2/refresh_tokens", RefreshTokenViewSet) router.register("oauth2/refresh_tokens", RefreshTokenViewSet)
@ -173,6 +176,7 @@ router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
router.register("propertymappings/saml", SAMLPropertyMappingViewSet) router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
router.register("propertymappings/scope", ScopeMappingViewSet) router.register("propertymappings/scope", ScopeMappingViewSet)
router.register("propertymappings/notification", NotificationWebhookMappingViewSet) router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
router.register("propertymappings/scim", SCIMMappingViewSet)
router.register("authenticators/all", DeviceViewSet, basename="device") router.register("authenticators/all", DeviceViewSet, basename="device")
router.register("authenticators/duo", DuoDeviceViewSet) router.register("authenticators/duo", DuoDeviceViewSet)

View File

@ -55,11 +55,11 @@ class AuthentikBlueprintsConfig(ManagedAppConfig):
"""Load v1 tasks""" """Load v1 tasks"""
self.import_module("authentik.blueprints.v1.tasks") self.import_module("authentik.blueprints.v1.tasks")
def reconcile_blueprints_discover(self): def reconcile_blueprints_discovery(self):
"""Run blueprint discovery""" """Run blueprint discovery"""
from authentik.blueprints.v1.tasks import blueprints_discover, clear_failed_blueprints from authentik.blueprints.v1.tasks import blueprints_discovery, clear_failed_blueprints
blueprints_discover.delay() blueprints_discovery.delay()
clear_failed_blueprints.delay() clear_failed_blueprints.delay()
def import_models(self): def import_models(self):

View File

@ -19,10 +19,8 @@ class Command(BaseCommand):
for blueprint_path in options.get("blueprints", []): for blueprint_path in options.get("blueprints", []):
content = BlueprintInstance(path=blueprint_path).retrieve() content = BlueprintInstance(path=blueprint_path).retrieve()
importer = Importer(content) importer = Importer(content)
valid, logs = importer.validate() valid, _ = importer.validate()
if not valid: if not valid:
for log in logs:
getattr(LOGGER, log.pop("log_level"))(**log)
self.stderr.write("blueprint invalid") self.stderr.write("blueprint invalid")
sys_exit(1) sys_exit(1)
importer.apply() importer.apply()

View File

@ -5,7 +5,7 @@ from authentik.lib.utils.time import fqdn_rand
CELERY_BEAT_SCHEDULE = { CELERY_BEAT_SCHEDULE = {
"blueprints_v1_discover": { "blueprints_v1_discover": {
"task": "authentik.blueprints.v1.tasks.blueprints_discover", "task": "authentik.blueprints.v1.tasks.blueprints_discovery",
"schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"), "schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"),
"options": {"queue": "authentik_scheduled"}, "options": {"queue": "authentik_scheduled"},
}, },

View File

@ -1,6 +1,5 @@
"""Blueprint helpers""" """Blueprint helpers"""
from functools import wraps from functools import wraps
from pathlib import Path
from typing import Callable from typing import Callable
from django.apps import apps from django.apps import apps
@ -45,13 +44,3 @@ def reconcile_app(app_name: str):
return wrapper return wrapper
return wrapper_outer return wrapper_outer
def load_yaml_fixture(path: str, **kwargs) -> str:
"""Load yaml fixture, optionally formatting it with kwargs"""
with open(Path(__file__).resolve().parent / Path(path), "r", encoding="utf-8") as _fixture:
fixture = _fixture.read()
try:
return fixture % kwargs
except TypeError:
return fixture

View File

@ -3,12 +3,12 @@ from os import environ
from django.test import TransactionTestCase from django.test import TransactionTestCase
from authentik.blueprints.tests import load_yaml_fixture
from authentik.blueprints.v1.exporter import FlowExporter from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import Importer, transaction_rollback from authentik.blueprints.v1.importer import Importer, transaction_rollback
from authentik.core.models import Group from authentik.core.models import Group
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
@ -113,14 +113,14 @@ class TestBlueprintsV1(TransactionTestCase):
"""Test export and import it twice""" """Test export and import it twice"""
count_initial = Prompt.objects.filter(field_key="username").count() count_initial = Prompt.objects.filter(field_key="username").count()
importer = Importer(load_yaml_fixture("fixtures/static_prompt_export.yaml")) importer = Importer(load_fixture("fixtures/static_prompt_export.yaml"))
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
count_before = Prompt.objects.filter(field_key="username").count() count_before = Prompt.objects.filter(field_key="username").count()
self.assertEqual(count_initial + 1, count_before) self.assertEqual(count_initial + 1, count_before)
importer = Importer(load_yaml_fixture("fixtures/static_prompt_export.yaml")) importer = Importer(load_fixture("fixtures/static_prompt_export.yaml"))
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before) self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before)
@ -130,7 +130,7 @@ class TestBlueprintsV1(TransactionTestCase):
ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete() ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete()
Group.objects.filter(name="test").delete() Group.objects.filter(name="test").delete()
environ["foo"] = generate_id() environ["foo"] = generate_id()
importer = Importer(load_yaml_fixture("fixtures/tags.yaml"), {"bar": "baz"}) importer = Importer(load_fixture("fixtures/tags.yaml"), {"bar": "baz"})
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first() policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first()

View File

@ -1,10 +1,10 @@
"""Test blueprints v1""" """Test blueprints v1"""
from django.test import TransactionTestCase from django.test import TransactionTestCase
from authentik.blueprints.tests import load_yaml_fixture
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import Importer
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
class TestBlueprintsV1Conditions(TransactionTestCase): class TestBlueprintsV1Conditions(TransactionTestCase):
@ -14,7 +14,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
"""Test conditions fulfilled""" """Test conditions fulfilled"""
flow_slug1 = generate_id() flow_slug1 = generate_id()
flow_slug2 = generate_id() flow_slug2 = generate_id()
import_yaml = load_yaml_fixture( import_yaml = load_fixture(
"fixtures/conditions_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2 "fixtures/conditions_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
) )
@ -31,7 +31,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
"""Test conditions not fulfilled""" """Test conditions not fulfilled"""
flow_slug1 = generate_id() flow_slug1 = generate_id()
flow_slug2 = generate_id() flow_slug2 = generate_id()
import_yaml = load_yaml_fixture( import_yaml = load_fixture(
"fixtures/conditions_not_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2 "fixtures/conditions_not_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
) )

View File

@ -1,10 +1,10 @@
"""Test blueprints v1""" """Test blueprints v1"""
from django.test import TransactionTestCase from django.test import TransactionTestCase
from authentik.blueprints.tests import load_yaml_fixture
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import Importer
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
class TestBlueprintsV1State(TransactionTestCase): class TestBlueprintsV1State(TransactionTestCase):
@ -13,7 +13,7 @@ class TestBlueprintsV1State(TransactionTestCase):
def test_state_present(self): def test_state_present(self):
"""Test state present""" """Test state present"""
flow_slug = generate_id() flow_slug = generate_id()
import_yaml = load_yaml_fixture("fixtures/state_present.yaml", id=flow_slug) import_yaml = load_fixture("fixtures/state_present.yaml", id=flow_slug)
importer = Importer(import_yaml) importer = Importer(import_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
@ -39,7 +39,7 @@ class TestBlueprintsV1State(TransactionTestCase):
def test_state_created(self): def test_state_created(self):
"""Test state created""" """Test state created"""
flow_slug = generate_id() flow_slug = generate_id()
import_yaml = load_yaml_fixture("fixtures/state_created.yaml", id=flow_slug) import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug)
importer = Importer(import_yaml) importer = Importer(import_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
@ -65,7 +65,7 @@ class TestBlueprintsV1State(TransactionTestCase):
def test_state_absent(self): def test_state_absent(self):
"""Test state absent""" """Test state absent"""
flow_slug = generate_id() flow_slug = generate_id()
import_yaml = load_yaml_fixture("fixtures/state_created.yaml", id=flow_slug) import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug)
importer = Importer(import_yaml) importer = Importer(import_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
@ -74,7 +74,7 @@ class TestBlueprintsV1State(TransactionTestCase):
flow: Flow = Flow.objects.filter(slug=flow_slug).first() flow: Flow = Flow.objects.filter(slug=flow_slug).first()
self.assertEqual(flow.slug, flow_slug) self.assertEqual(flow.slug, flow_slug)
import_yaml = load_yaml_fixture("fixtures/state_absent.yaml", id=flow_slug) import_yaml = load_fixture("fixtures/state_absent.yaml", id=flow_slug)
importer = Importer(import_yaml) importer = Importer(import_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())

View File

@ -6,7 +6,7 @@ from django.test import TransactionTestCase
from yaml import dump from yaml import dump
from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discover, blueprints_find from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discovery, blueprints_find
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
@ -53,7 +53,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
file.seek(0) file.seek(0)
file_hash = sha512(file.read().encode()).hexdigest() file_hash = sha512(file.read().encode()).hexdigest()
file.flush() file.flush()
blueprints_discover() # pylint: disable=no-value-for-parameter blueprints_discovery() # pylint: disable=no-value-for-parameter
instance = BlueprintInstance.objects.filter(name=blueprint_id).first() instance = BlueprintInstance.objects.filter(name=blueprint_id).first()
self.assertEqual(instance.last_applied_hash, file_hash) self.assertEqual(instance.last_applied_hash, file_hash)
self.assertEqual( self.assertEqual(
@ -81,7 +81,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
) )
) )
file.flush() file.flush()
blueprints_discover() # pylint: disable=no-value-for-parameter blueprints_discovery() # pylint: disable=no-value-for-parameter
blueprint = BlueprintInstance.objects.filter(name="foo").first() blueprint = BlueprintInstance.objects.filter(name="foo").first()
self.assertEqual( self.assertEqual(
blueprint.last_applied_hash, blueprint.last_applied_hash,
@ -106,7 +106,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
) )
) )
file.flush() file.flush()
blueprints_discover() # pylint: disable=no-value-for-parameter blueprints_discovery() # pylint: disable=no-value-for-parameter
blueprint.refresh_from_db() blueprint.refresh_from_db()
self.assertEqual( self.assertEqual(
blueprint.last_applied_hash, blueprint.last_applied_hash,

View File

@ -40,6 +40,10 @@ from authentik.lib.models import SerializerModel
from authentik.outposts.models import OutpostServiceConnection from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel from authentik.policies.models import Policy, PolicyBindingModel
# Context set when the serializer is created in a blueprint context
# Update website/developer-docs/blueprints/v1/models.md when used
SERIALIZER_CONTEXT_BLUEPRINT = "blueprint_entry"
def is_model_allowed(model: type[Model]) -> bool: def is_model_allowed(model: type[Model]) -> bool:
"""Check if model is allowed""" """Check if model is allowed"""
@ -158,7 +162,12 @@ class Importer:
raise EntryInvalidError(f"Model {model} not allowed") raise EntryInvalidError(f"Model {model} not allowed")
if issubclass(model, BaseMetaModel): if issubclass(model, BaseMetaModel):
serializer_class: type[Serializer] = model.serializer() serializer_class: type[Serializer] = model.serializer()
serializer = serializer_class(data=entry.get_attrs(self.__import)) serializer = serializer_class(
data=entry.get_attrs(self.__import),
context={
SERIALIZER_CONTEXT_BLUEPRINT: entry,
},
)
try: try:
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
except ValidationError as exc: except ValidationError as exc:
@ -217,7 +226,12 @@ class Importer:
always_merger.merge(full_data, updated_identifiers) always_merger.merge(full_data, updated_identifiers)
serializer_kwargs["data"] = full_data serializer_kwargs["data"] = full_data
serializer: Serializer = model().serializer(**serializer_kwargs) serializer: Serializer = model().serializer(
context={
SERIALIZER_CONTEXT_BLUEPRINT: entry,
},
**serializer_kwargs,
)
try: try:
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
except ValidationError as exc: except ValidationError as exc:

View File

@ -76,7 +76,7 @@ class BlueprintEventHandler(FileSystemEventHandler):
return return
if isinstance(event, FileCreatedEvent): if isinstance(event, FileCreatedEvent):
LOGGER.debug("new blueprint file created, starting discovery") LOGGER.debug("new blueprint file created, starting discovery")
blueprints_discover.delay() blueprints_discovery.delay()
if isinstance(event, FileModifiedEvent): if isinstance(event, FileModifiedEvent):
path = Path(event.src_path) path = Path(event.src_path)
root = Path(CONFIG.y("blueprints_dir")).absolute() root = Path(CONFIG.y("blueprints_dir")).absolute()
@ -134,7 +134,7 @@ def blueprints_find():
throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True
) )
@prefill_task @prefill_task
def blueprints_discover(self: MonitoredTask): def blueprints_discovery(self: MonitoredTask):
"""Find blueprints and check if they need to be created in the database""" """Find blueprints and check if they need to be created in the database"""
count = 0 count = 0
for blueprint in blueprints_find(): for blueprint in blueprints_find():

View File

@ -37,7 +37,6 @@ from authentik.lib.utils.file import (
from authentik.policies.api.exec import PolicyTestResultSerializer from authentik.policies.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult from authentik.policies.types import PolicyResult
from authentik.stages.user_login.stage import USER_LOGIN_AUTHENTICATED
LOGGER = get_logger() LOGGER = get_logger()
@ -186,10 +185,6 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
if superuser_full_list and request.user.is_superuser: if superuser_full_list and request.user.is_superuser:
return super().list(request) 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()) queryset = self._filter_queryset_for_list(self.get_queryset())
self.paginate_queryset(queryset) self.paginate_queryset(queryset)

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerSuperuserPermissions from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.blueprints.api import ManagedSerializer from authentik.blueprints.api import ManagedSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
@ -29,9 +30,20 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
user_obj = UserSerializer(required=False, source="user", read_only=True) user_obj = UserSerializer(required=False, source="user", read_only=True)
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["key"] = CharField()
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created.""" """Ensure only API or App password tokens are created."""
request: Request = self.context["request"] 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("user", request.user)
attrs.setdefault("intent", TokenIntents.INTENT_API) attrs.setdefault("intent", TokenIntents.INTENT_API)
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]: if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:

View File

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

View File

@ -1,8 +1,9 @@
"""Property Mapping Evaluator""" """Property Mapping Evaluator"""
from typing import Optional from typing import Any, Optional
from django.db.models import Model from django.db.models import Model
from django.http import HttpRequest from django.http import HttpRequest
from prometheus_client import Histogram
from authentik.core.models import User from authentik.core.models import User
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
@ -10,6 +11,12 @@ from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.errors import exception_to_string
from authentik.policies.types import PolicyRequest 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): class PropertyMappingEvaluator(BaseEvaluator):
"""Custom Evaluator that adds some different context variables.""" """Custom Evaluator that adds some different context variables."""
@ -49,3 +56,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
event.from_http(req.http_request, req.user) event.from_http(req.http_request, req.user)
return return
event.save() 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 db_alias = schema_editor.connection.alias
akadmin, _ = User.objects.using(db_alias).get_or_create( 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 password = None
if "TF_BUILD" in environ or settings.TEST: if "TF_BUILD" in environ or settings.TEST:
password = "akadmin" # noqa # nosec password = "akadmin" # noqa # nosec
if "AK_ADMIN_PASS" in environ:
password = environ["AK_ADMIN_PASS"]
if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ: if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ:
password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"] password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]
if 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") akadmin = User.objects.using(db_alias).filter(username="akadmin")
if not akadmin.exists(): if not akadmin.exists():
return return
key = None if "AUTHENTIK_BOOTSTRAP_TOKEN" not in environ:
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:
return return
key = environ["AUTHENTIK_BOOTSTRAP_TOKEN"]
Token.objects.using(db_alias).create( Token.objects.using(db_alias).create(
identifier="authentik-bootstrap-token", identifier="authentik-bootstrap-token",
user=akadmin.first(), user=akadmin.first(),
@ -186,7 +182,9 @@ class Migration(migrations.Migration):
model_name="application", model_name="application",
name="meta_launch_url", name="meta_launch_url",
field=models.TextField( field=models.TextField(
blank=True, default="", validators=[authentik.lib.models.DomainlessURLValidator()] blank=True,
default="",
validators=[authentik.lib.models.DomainlessFormattedURLValidator()],
), ),
), ),
migrations.RunPython( 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.blueprints.models import ManagedModel
from authentik.core.exceptions import PropertyMappingExpressionException from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.signals import password_changed
from authentik.core.types import UILoginButton, UserSettingSerializer from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.lib.avatars import get_avatar from authentik.lib.avatars import get_avatar
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id 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.lib.utils.http import get_client_ip
from authentik.policies.models import PolicyBindingModel from authentik.policies.models import PolicyBindingModel
@ -189,6 +192,8 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
def set_password(self, raw_password, signal=True): def set_password(self, raw_password, signal=True):
if self.pk and signal: if self.pk and signal:
from authentik.core.signals import password_changed
password_changed.send(sender=self, user=self, password=raw_password) password_changed.send(sender=self, user=self, password=raw_password)
self.password_change_date = now() self.password_change_date = now()
return super().set_password(raw_password) return super().set_password(raw_password)
@ -242,11 +247,12 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
class Provider(SerializerModel): class Provider(SerializerModel):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application""" """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
name = models.TextField() name = models.TextField(unique=True)
authorization_flow = models.ForeignKey( authorization_flow = models.ForeignKey(
"authentik_flows.Flow", "authentik_flows.Flow",
on_delete=models.CASCADE, on_delete=models.CASCADE,
null=True,
help_text=_("Flow used when authorizing this provider."), help_text=_("Flow used when authorizing this provider."),
related_name="provider_authorization", related_name="provider_authorization",
) )
@ -289,7 +295,7 @@ class Application(SerializerModel, PolicyBindingModel):
) )
meta_launch_url = models.TextField( meta_launch_url = models.TextField(
default="", blank=True, validators=[DomainlessURLValidator()] default="", blank=True, validators=[DomainlessFormattedURLValidator()]
) )
open_in_new_tab = models.BooleanField( 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.""" """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) pm_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
name = models.TextField() name = models.TextField(unique=True)
expression = models.TextField() expression = models.TextField()
objects = InheritanceManager() objects = InheritanceManager()
@ -629,7 +635,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
try: try:
return evaluator.evaluate(self.expression) return evaluator.evaluate(self.expression)
except Exception as exc: except Exception as exc:
raise PropertyMappingExpressionException(str(exc)) from exc raise PropertyMappingExpressionException(exc) from exc
def __str__(self): def __str__(self):
return f"Property Mapping {self.name}" 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.dispatch import receiver
from django.http.request import HttpRequest from django.http.request import HttpRequest
from authentik.core.models import Application, AuthenticatedSession
# Arguments: user: User, password: str # Arguments: user: User, password: str
password_changed = Signal() password_changed = Signal()
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage # Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
login_failed = Signal() login_failed = Signal()
if TYPE_CHECKING: 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, **_): def post_save_application(sender: type[Model], instance, created: bool, **_):
"""Clear user's application cache upon application creation""" """Clear user's application cache upon application creation"""
from authentik.core.api.applications import user_app_cache_key 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 if not created: # pragma: no cover
return return
# Also delete user application cache # Also delete user application cache
keys = cache.keys(user_app_cache_key("*")) keys = cache.keys(user_app_cache_key("*"))
cache.delete_many(keys) cache.delete_many(keys)
@ -37,7 +37,6 @@ def post_save_application(sender: type[Model], instance, created: bool, **_):
@receiver(user_logged_in) @receiver(user_logged_in)
def user_logged_in_session(sender, request: HttpRequest, user: "User", **_): def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
"""Create an AuthenticatedSession from request""" """Create an AuthenticatedSession from request"""
from authentik.core.models import AuthenticatedSession
session = AuthenticatedSession.from_request(request, user) session = AuthenticatedSession.from_request(request, user)
if session: if session:
@ -47,18 +46,11 @@ def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
@receiver(user_logged_out) @receiver(user_logged_out)
def user_logged_out_session(sender, request: HttpRequest, user: "User", **_): def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
"""Delete AuthenticatedSession if it exists""" """Delete AuthenticatedSession if it exists"""
from authentik.core.models import AuthenticatedSession
AuthenticatedSession.objects.filter(session_key=request.session.session_key).delete() 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", **_): def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
"""Delete session when authenticated session is deleted""" """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_key = f"{KEY_PREFIX}{instance.session_key}"
cache.delete(cache_key) cache.delete(cache_key)

View File

@ -16,7 +16,8 @@
{% block head_before %} {% block head_before %}
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> <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> <script src="{% static 'dist/poly.js' %}" type="module"></script>
{% block head %} {% block head %}
{% endblock %} {% endblock %}

View File

@ -37,6 +37,22 @@ class TestApplicationsAPI(APITestCase):
order=0, 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.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.test.goauthentik.io/{self.user.username}",
)
def test_set_icon(self): def test_set_icon(self):
"""Test set_icon""" """Test set_icon"""
file = ContentFile(b"text", "name") 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 guardian.shortcuts import get_anonymous_user
from rest_framework.test import APITestCase 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.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
from authentik.core.tests.utils import create_test_admin_user from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
@ -99,3 +100,16 @@ class TestTokenAPI(APITestCase):
self.assertEqual(len(body["results"]), 2) self.assertEqual(len(body["results"]), 2)
self.assertEqual(body["results"][0]["identifier"], token_should.identifier) self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
self.assertEqual(body["results"][1]["identifier"], token_should_not.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""" """Test Users API"""
from datetime import datetime
from django.contrib.sessions.backends.cache import KEY_PREFIX from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache from django.core.cache import cache
from django.urls.base import reverse from django.urls.base import reverse
from rest_framework.test import APITestCase 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.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
from authentik.flows.models import FlowDesignation from authentik.flows.models import FlowDesignation
from authentik.lib.generators import generate_id, generate_key from authentik.lib.generators import generate_id, generate_key
@ -130,7 +138,71 @@ class TestUsersAPI(APITestCase):
}, },
) )
self.assertEqual(response.status_code, 200) 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): def test_service_account_invalid(self):
"""Service account creation (twice with same name, expect error)""" """Service account creation (twice with same name, expect error)"""
@ -143,7 +215,19 @@ class TestUsersAPI(APITestCase):
}, },
) )
self.assertEqual(response.status_code, 200) 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( response = self.client.post(
reverse("authentik_api:user-service-account"), reverse("authentik_api:user-service-account"),
data={ data={

View File

@ -11,6 +11,7 @@ from authentik.flows.challenge import (
HttpChallengeResponse, HttpChallengeResponse,
RedirectChallenge, RedirectChallenge,
) )
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
@ -41,6 +42,7 @@ class RedirectToAppLaunch(View):
flow = tenant.flow_authentication flow = tenant.flow_authentication
planner = FlowPlanner(flow) planner = FlowPlanner(flow)
planner.allow_empty_flows = True planner.allow_empty_flows = True
try:
plan = planner.plan( plan = planner.plan(
request, request,
{ {
@ -50,6 +52,8 @@ class RedirectToAppLaunch(View):
PLAN_CONTEXT_CONSENT_PERMISSIONS: [], PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
}, },
) )
except FlowNonApplicableException:
raise Http404
plan.insert_stage(in_memory_stage(RedirectToAppStage)) plan.insert_stage(in_memory_stage(RedirectToAppStage))
request.session[SESSION_KEY_PLAN] = plan request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug) 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.contrib.sessions.models import Session
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.db.models import Model 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.http import HttpRequest, HttpResponse
from django_otp.plugins.otp_static.models import StaticToken from django_otp.plugins.otp_static.models import StaticToken
from guardian.models import UserObjectPermission from guardian.models import UserObjectPermission
from authentik.core.models import ( from authentik.core.models import (
AuthenticatedSession, AuthenticatedSession,
Group,
PropertyMapping, PropertyMapping,
Provider, Provider,
Source, Source,
@ -28,6 +29,7 @@ from authentik.lib.utils.errors import exception_to_string
from authentik.outposts.models import OutpostServiceConnection from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel from authentik.policies.models import Policy, PolicyBindingModel
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
from authentik.providers.scim.models import SCIMGroup, SCIMUser
IGNORED_MODELS = ( IGNORED_MODELS = (
Event, Event,
@ -48,6 +50,8 @@ IGNORED_MODELS = (
AuthorizationCode, AuthorizationCode,
AccessToken, AccessToken,
RefreshToken, RefreshToken,
SCIMUser,
SCIMGroup,
) )
@ -58,6 +62,13 @@ def should_log_model(model: Model) -> bool:
return model.__class__ not in IGNORED_MODELS 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): class EventNewThread(Thread):
"""Create Event in background thread""" """Create Event in background thread"""
@ -96,6 +107,7 @@ class AuditMiddleware:
return return
post_save_handler = partial(self.post_save_handler, user=request.user, request=request) 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) 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.connect(
post_save_handler, post_save_handler,
dispatch_uid=request.request_id, dispatch_uid=request.request_id,
@ -106,6 +118,11 @@ class AuditMiddleware:
dispatch_uid=request.request_id, dispatch_uid=request.request_id,
weak=False, weak=False,
) )
m2m_changed.connect(
m2m_changed_handler,
dispatch_uid=request.request_id,
weak=False,
)
def disconnect(self, request: HttpRequest): def disconnect(self, request: HttpRequest):
"""Disconnect signals""" """Disconnect signals"""
@ -113,6 +130,7 @@ class AuditMiddleware:
return return
post_save.disconnect(dispatch_uid=request.request_id) post_save.disconnect(dispatch_uid=request.request_id)
pre_delete.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: def __call__(self, request: HttpRequest) -> HttpResponse:
self.connect(request) self.connect(request)
@ -167,3 +185,20 @@ class AuditMiddleware:
user=user, user=user,
model=model_to_dict(instance), model=model_to_dict(instance),
).run() ).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 import uuid
from datetime import timedelta from datetime import timedelta
from typing import Iterable
import django.db.models.deletion import django.db.models.deletion
from django.apps.registry import Apps from django.apps.registry import Apps
@ -13,6 +12,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
import authentik.events.models import authentik.events.models
import authentik.lib.models import authentik.lib.models
from authentik.events.models import EventAction, NotificationSeverity, TransportMode 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): 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"]) 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): def update_expires(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias db_alias = schema_editor.connection.alias
Event = apps.get_model("authentik_events", "event") Event = apps.get_model("authentik_events", "event")

View File

@ -41,7 +41,7 @@ class TaskResult:
def with_error(self, exc: Exception) -> "TaskResult": def with_error(self, exc: Exception) -> "TaskResult":
"""Since errors might not always be pickle-able, set the traceback""" """Since errors might not always be pickle-able, set the traceback"""
self.messages.append(str(exc)) self.messages.append(exception_to_string(exc))
return self return self
@ -111,6 +111,7 @@ class MonitoredTask(Task):
_result: Optional[TaskResult] _result: Optional[TaskResult]
_uid: Optional[str] _uid: Optional[str]
start: Optional[float] = None
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -118,7 +119,6 @@ class MonitoredTask(Task):
self._uid = None self._uid = None
self._result = None self._result = None
self.result_timeout_hours = 6 self.result_timeout_hours = 6
self.start = default_timer()
def set_uid(self, uid: str): def set_uid(self, uid: str):
"""Set UID, so in the case of an unexpected error its saved correctly""" """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.""" """Set result for current run, will overwrite previous result."""
self._result = 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 # pylint: disable=too-many-arguments
def after_return(self, status, retval, task_id, args: list[Any], kwargs: dict[str, Any], einfo): 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) super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
@ -138,7 +142,7 @@ class MonitoredTask(Task):
info = TaskInfo( info = TaskInfo(
task_name=self.__name__, task_name=self.__name__,
task_description=self.__doc__, task_description=self.__doc__,
start_timestamp=self.start, start_timestamp=self.start or default_timer(),
finish_timestamp=default_timer(), finish_timestamp=default_timer(),
finish_time=datetime.now(), finish_time=datetime.now(),
result=self._result, result=self._result,
@ -162,7 +166,7 @@ class MonitoredTask(Task):
TaskInfo( TaskInfo(
task_name=self.__name__, task_name=self.__name__,
task_description=self.__doc__, task_description=self.__doc__,
start_timestamp=self.start, start_timestamp=self.start or default_timer(),
finish_timestamp=default_timer(), finish_timestamp=default_timer(),
finish_time=datetime.now(), finish_time=datetime.now(),
result=self._result, result=self._result,

View File

@ -1,4 +1,7 @@
"""Flow Binding API Views""" """Flow Binding API Views"""
from typing import Any
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
@ -12,6 +15,13 @@ class FlowStageBindingSerializer(ModelSerializer):
stage_obj = StageSerializer(read_only=True, source="stage") 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: class Meta:
model = FlowStageBinding model = FlowStageBinding
fields = [ fields = [

View File

@ -8,7 +8,7 @@ from rest_framework.serializers import CharField
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.models import Flow, FlowStageBinding from authentik.flows.models import Flow, FlowAuthenticationRequirement, FlowStageBinding
@dataclass @dataclass
@ -160,12 +160,37 @@ class FlowDiagram:
) )
return stages + elements 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: def build(self) -> str:
"""Build flowchart""" """Build flowchart"""
all_elements = [ all_elements = [
"graph TD", "graph TD",
] ]
all_elements.extend(self.get_flow_auth_requirement())
pre_flow_policies_element = DiagramElement( pre_flow_policies_element = DiagramElement(
"flow_pre", _("Pre-flow policies"), style=["[[", "]]"] "flow_pre", _("Pre-flow policies"), style=["[[", "]]"]
) )
@ -179,6 +204,7 @@ class FlowDiagram:
_("End of the flow"), _("End of the flow"),
_("Policy denied"), _("Policy denied"),
flow_policies, 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) stage = InheritanceForeignKey(Stage, on_delete=models.CASCADE)
evaluate_on_plan = models.BooleanField( evaluate_on_plan = models.BooleanField(
default=True, default=False,
help_text=_( help_text=_("Evaluate policies during the Flow planning process."),
"Evaluate policies during the Flow planning process. "
"Disable this for input-based policies."
),
) )
re_evaluate_policies = models.BooleanField( re_evaluate_policies = models.BooleanField(
default=False, default=True,
help_text=_("Evaluate policies when the Stage is present to the user."), help_text=_("Evaluate policies when the Stage is present to the user."),
) )

View File

@ -147,7 +147,6 @@ class FlowPlanner:
) -> FlowPlan: ) -> FlowPlan:
"""Check each of the flows' policies, check policies for each stage with PolicyBinding """Check each of the flows' policies, check policies for each stage with PolicyBinding
and return ordered list""" and return ordered list"""
self._check_authentication(request)
with Hub.current.start_span( with Hub.current.start_span(
op="authentik.flow.planner.plan", description=self.flow.slug op="authentik.flow.planner.plan", description=self.flow.slug
) as span: ) as span:
@ -165,6 +164,12 @@ class FlowPlanner:
user = default_context[PLAN_CONTEXT_PENDING_USER] user = default_context[PLAN_CONTEXT_PENDING_USER]
else: else:
user = request.user 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 # First off, check the flow's direct policy bindings
# to make sure the user even has access to the flow # to make sure the user even has access to the flow
engine = PolicyEngine(self.flow, user, request) engine = PolicyEngine(self.flow, user, request)
@ -261,7 +266,6 @@ class FlowPlanner:
marker = ReevaluateMarker(binding=binding) marker = ReevaluateMarker(binding=binding)
if stage: if stage:
plan.append(binding, marker) plan.append(binding, marker)
HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug)
self._logger.debug( self._logger.debug(
"f(plan): finished building", "f(plan): finished building",
) )

View File

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

View File

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

View File

@ -1,10 +1,11 @@
"""Avatar utils""" """Avatar utils"""
from base64 import b64encode from base64 import b64encode
from functools import cache from functools import cache as funccache
from hashlib import md5 from hashlib import md5
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from urllib.parse import urlencode from urllib.parse import urlencode
from django.core.cache import cache
from django.templatetags.static import static from django.templatetags.static import static
from lxml import etree # nosec from lxml import etree # nosec
from lxml.etree import Element, SubElement # 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" GRAVATAR_URL = "https://secure.gravatar.com"
DEFAULT_AVATAR = static("dist/assets/images/user_default.png") DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
CACHE_KEY_GRAVATAR = "goauthentik.io/lib/avatars/"
if TYPE_CHECKING: if TYPE_CHECKING:
from authentik.core.models import User 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")] parameters = [("size", "158"), ("rating", "g"), ("default", "404")]
gravatar_url = f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}" gravatar_url = f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}"
@cache full_key = CACHE_KEY_GRAVATAR + mail_hash
def check_non_default(url: str): if cache.has_key(full_key):
"""Cache HEAD check, based on URL""" cache.touch(full_key)
return cache.get(full_key)
try: try:
# Since we specify a default of 404, do a HEAD request # Since we specify a default of 404, do a HEAD request
# (HEAD since we don't need the body) # (HEAD since we don't need the body)
# so if that returns a 404, move onto the next mode # so if that returns a 404, move onto the next mode
res = get_http_session().head(url, timeout=5) res = get_http_session().head(gravatar_url, timeout=5)
if res.status_code == 404: if res.status_code == 404:
cache.set(full_key, None)
return None return None
res.raise_for_status() res.raise_for_status()
except RequestException: except RequestException:
return url return gravatar_url
return url cache.set(full_key, gravatar_url)
return gravatar_url
return check_non_default(gravatar_url)
def generate_colors(text: str) -> tuple[str, str]: 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 return bg_hex, text_hex
@cache @funccache
# pylint: disable=too-many-arguments,too-many-locals # pylint: disable=too-many-arguments,too-many-locals
def generate_avatar_from_name( def generate_avatar_from_name(
user: "User", name: str,
length: int = 2, length: int = 2,
size: int = 64, size: int = 64,
rounded: bool = False, rounded: bool = False,
@ -98,8 +102,6 @@ def generate_avatar_from_name(
Inspired from: https://github.com/LasseRafn/ui-avatars Inspired from: https://github.com/LasseRafn/ui-avatars
""" """
name = user.name if user.name != "" else "a k"
name_parts = name.split() name_parts = name.split()
# Only abbreviate first and last name # Only abbreviate first and last name
if len(name_parts) > 2: if len(name_parts) > 2:
@ -152,7 +154,7 @@ def generate_avatar_from_name(
def avatar_mode_generated(user: "User", mode: str) -> Optional[str]: def avatar_mode_generated(user: "User", mode: str) -> Optional[str]:
"""Wrapper that converts generated avatar to base64 svg""" """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')}" 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""" """authentik expression policy evaluator"""
import re import re
import socket
from ipaddress import ip_address, ip_network from ipaddress import ip_address, ip_network
from textwrap import indent from textwrap import indent
from typing import Any, Iterable, Optional from typing import Any, Iterable, Optional
from cachetools import TLRUCache, cached
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django_otp import devices_for_user from django_otp import devices_for_user
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
@ -41,6 +43,8 @@ class BaseEvaluator:
"ak_is_group_member": BaseEvaluator.expr_is_group_member, "ak_is_group_member": BaseEvaluator.expr_is_group_member,
"ak_user_by": BaseEvaluator.expr_user_by, "ak_user_by": BaseEvaluator.expr_user_by,
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator, "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_create_event": self.expr_event_create,
"ak_logger": get_logger(self._filename).bind(), "ak_logger": get_logger(self._filename).bind(),
"requests": get_http_session(), "requests": get_http_session(),
@ -49,6 +53,39 @@ class BaseEvaluator:
} }
self._context = {} 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 @staticmethod
def expr_flatten(value: list[Any] | Any) -> Optional[Any]: def expr_flatten(value: list[Any] | Any) -> Optional[Any]:
"""Flatten `value` if its a list""" """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,22 @@ class DomainlessURLValidator(URLValidator):
if scheme not in self.schemes: if scheme not in self.schemes:
value = "default" + value value = "default" + value
super().__call__(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.formatter_re = r"([%\(\)a-zA-Z])*"
self.host_re = "(" + self.formatter_re + self.hostname_re + self.domain_re + "|localhost)"
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 billiard.exceptions import SoftTimeLimitExceeded, WorkerLostError
from celery.exceptions import CeleryError from celery.exceptions import CeleryError
from channels.middleware import BaseMiddleware
from channels_redis.core import ChannelFull from channels_redis.core import ChannelFull
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError 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 ConnectionError as RedisConnectionError
from redis.exceptions import RedisError, ResponseError from redis.exceptions import RedisError, ResponseError
from rest_framework.exceptions import APIException 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 import init as sentry_sdk_init
from sentry_sdk.api import set_tag from sentry_sdk.api import set_tag
from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.threading import ThreadingIntegration from sentry_sdk.integrations.threading import ThreadingIntegration
from sentry_sdk.tracing import Transaction
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from websockets.exceptions import WebSocketException from websockets.exceptions import WebSocketException
from authentik import __version__, get_build_hash from authentik import __version__, get_build_hash
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.utils.http import authentik_user_agent 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() 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): class SentryIgnoredException(Exception):
"""Base Class for all errors that are suppressed, and not sent to sentry.""" """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: def traces_sampler(sampling_context: dict) -> float:
"""Custom sampler to ignore certain routes""" """Custom sampler to ignore certain routes"""
path = sampling_context.get("asgi_scope", {}).get("path", "") path = sampling_context.get("asgi_scope", {}).get("path", "")
_type = sampling_context.get("asgi_scope", {}).get("type", "")
# Ignore all healthcheck routes # Ignore all healthcheck routes
if path.startswith("/-/health") or path.startswith("/-/metrics"): if path.startswith("/-/health") or path.startswith("/-/metrics"):
return 0 return 0
if _type == "websocket":
return 0
return float(CONFIG.y("error_reporting.sample_rate", 0.1)) return float(CONFIG.y("error_reporting.sample_rate", 0.1))

View File

@ -1,4 +1,7 @@
"""Test utils""" """Test utils"""
from inspect import currentframe
from pathlib import Path
from django.contrib.messages.middleware import MessageMiddleware from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.http import HttpRequest from django.http import HttpRequest
@ -11,6 +14,21 @@ def dummy_get_response(request: HttpRequest): # pragma: no cover
return None return None
def load_fixture(path: str, **kwargs) -> str:
"""Load fixture, optionally formatting it with kwargs"""
current = currentframe()
parent = current.f_back
calling_file_path = parent.f_globals["__file__"]
with open(
Path(calling_file_path).resolve().parent / Path(path), "r", encoding="utf-8"
) as _fixture:
fixture = _fixture.read()
try:
return fixture % kwargs
except TypeError:
return fixture
def get_request(*args, user=None, **kwargs): def get_request(*args, user=None, **kwargs):
"""Get a request with usable session""" """Get a request with usable session"""
request = RequestFactory().get(*args, **kwargs) request = RequestFactory().get(*args, **kwargs)

View File

@ -38,13 +38,17 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
if OUTPOST_REMOTE_IP_HEADER not in request.META or OUTPOST_TOKEN_HEADER not in request.META: if OUTPOST_REMOTE_IP_HEADER not in request.META or OUTPOST_TOKEN_HEADER not in request.META:
return None return None
fake_ip = request.META[OUTPOST_REMOTE_IP_HEADER] fake_ip = request.META[OUTPOST_REMOTE_IP_HEADER]
tokens = Token.filter_not_expired( token = (
Token.filter_not_expired(
key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API
) )
if not tokens.exists(): .select_related("user")
.first()
)
if not token:
LOGGER.warning("Attempted remote-ip override without token", fake_ip=fake_ip) LOGGER.warning("Attempted remote-ip override without token", fake_ip=fake_ip)
return None return None
user = tokens.first().user user = token.user
if not user.group_attributes(request).get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False): if not user.group_attributes(request).get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
LOGGER.warning( LOGGER.warning(
"Remote-IP override: user doesn't have permission", "Remote-IP override: user doesn't have permission",

View File

@ -9,4 +9,4 @@ def get_lxml_parser():
def lxml_from_string(text: str): def lxml_from_string(text: str):
"""Wrapper around fromstring""" """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: if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController from authentik.outposts.controllers.kubernetes import KubernetesController
# pylint: disable=invalid-name
T = TypeVar("T", V1Pod, V1Deployment) T = TypeVar("T", V1Pod, V1Deployment)
@ -56,6 +55,7 @@ class KubernetesObjectReconciler(Generic[T]):
} }
).lower() ).lower()
# pylint: disable=invalid-name
def up(self): def up(self):
"""Create object if it doesn't exist, update if needed or recreate if needed.""" """Create object if it doesn't exist, update if needed or recreate if needed."""
current = None 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""" """Connection details for an Outpost Controller, like Docker or Kubernetes"""
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True) uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
name = models.TextField() name = models.TextField(unique=True)
local = models.BooleanField( local = models.BooleanField(
default=False, default=False,
@ -239,7 +239,7 @@ class Outpost(SerializerModel, ManagedModel):
"""Outpost instance which manages a service user and token""" """Outpost instance which manages a service user and token"""
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True) 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) type = models.TextField(choices=OutpostType.choices, default=OutpostType.PROXY)
service_connection = InheritanceForeignKey( service_connection = InheritanceForeignKey(

View File

@ -19,9 +19,9 @@ CELERY_BEAT_SCHEDULE = {
"schedule": crontab(minute=fqdn_rand("outpost_token_ensurer"), hour="*/8"), "schedule": crontab(minute=fqdn_rand("outpost_token_ensurer"), hour="*/8"),
"options": {"queue": "authentik_scheduled"}, "options": {"queue": "authentik_scheduled"},
}, },
"outpost_local_connection": { "outpost_connection_discovery": {
"task": "authentik.outposts.tasks.outpost_local_connection", "task": "authentik.outposts.tasks.outpost_connection_discovery",
"schedule": crontab(minute=fqdn_rand("outpost_local_connection"), hour="*/8"), "schedule": crontab(minute=fqdn_rand("outpost_connection_discovery"), hour="*/8"),
"options": {"queue": "authentik_scheduled"}, "options": {"queue": "authentik_scheduled"},
}, },
} }

View File

@ -7,7 +7,6 @@ from urllib.parse import urlparse
import yaml import yaml
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.core.cache import cache from django.core.cache import cache
from django.db import DatabaseError, InternalError, ProgrammingError from django.db import DatabaseError, InternalError, ProgrammingError
from django.db.models.base import Model 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.docker import ProxyDockerController
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
from authentik.root.messages.storage import closing_send
LOGGER = get_logger() LOGGER = get_logger()
CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s" CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s"
@ -217,50 +217,52 @@ def outpost_post_save(model_class: str, model_pk: Any):
def outpost_send_update(model_instace: Model): def outpost_send_update(model_instace: Model):
"""Send outpost update to all registered outposts, regardless to which authentik """Send outpost update to all registered outposts, regardless to which authentik
instance they are connected""" instance they are connected"""
channel_layer = get_channel_layer()
if isinstance(model_instace, OutpostModel): if isinstance(model_instace, OutpostModel):
for outpost in model_instace.outpost_set.all(): for outpost in model_instace.outpost_set.all():
_outpost_single_update(outpost, channel_layer) _outpost_single_update(outpost)
elif isinstance(model_instace, 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""" """Update outpost instances connected to a single outpost"""
# Ensure token again, because this function is called when anything related to an # Ensure token again, because this function is called when anything related to an
# OutpostModel is saved, so we can be sure permissions are right # OutpostModel is saved, so we can be sure permissions are right
_ = outpost.token _ = outpost.token
outpost.build_user_permissions(outpost.user) outpost.build_user_permissions(outpost.user)
if not layer: # pragma: no cover
layer = get_channel_layer()
for state in OutpostState.for_outpost(outpost): for state in OutpostState.for_outpost(outpost):
for channel in state.channel_ids: for channel in state.channel_ids:
LOGGER.debug("sending update", channel=channel, instance=state.uid, outpost=outpost) 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() @CELERY_APP.task(
def outpost_local_connection(): base=MonitoredTask,
bind=True,
)
def outpost_connection_discovery(self: MonitoredTask):
"""Checks the local environment and create Service connections.""" """Checks the local environment and create Service connections."""
status = TaskResult(TaskResultStatus.SUCCESSFUL)
if not CONFIG.y_bool("outposts.discover"): if not CONFIG.y_bool("outposts.discover"):
LOGGER.info("Outpost integration discovery is disabled") status.messages.append("Outpost integration discovery is disabled")
self.set_status(status)
return return
# Explicitly check against token filename, as that's # Explicitly check against token filename, as that's
# only present when the integration is enabled # only present when the integration is enabled
if Path(SERVICE_TOKEN_FILENAME).exists(): if Path(SERVICE_TOKEN_FILENAME).exists():
LOGGER.info("Detected in-cluster Kubernetes Config") status.messages.append("Detected in-cluster Kubernetes Config")
if not KubernetesServiceConnection.objects.filter(local=True).exists(): if not KubernetesServiceConnection.objects.filter(local=True).exists():
LOGGER.debug("Created Service Connection for in-cluster") status.messages.append("Created Service Connection for in-cluster")
KubernetesServiceConnection.objects.create( KubernetesServiceConnection.objects.create(
name="Local Kubernetes Cluster", local=True, kubeconfig={} name="Local Kubernetes Cluster", local=True, kubeconfig={}
) )
# For development, check for the existence of a kubeconfig file # For development, check for the existence of a kubeconfig file
kubeconfig_path = Path(KUBE_CONFIG_DEFAULT_LOCATION).expanduser() kubeconfig_path = Path(KUBE_CONFIG_DEFAULT_LOCATION).expanduser()
if kubeconfig_path.exists(): if kubeconfig_path.exists():
LOGGER.info("Detected kubeconfig") status.messages.append("Detected kubeconfig")
kubeconfig_local_name = f"k8s-{gethostname()}" kubeconfig_local_name = f"k8s-{gethostname()}"
if not KubernetesServiceConnection.objects.filter(name=kubeconfig_local_name).exists(): if not KubernetesServiceConnection.objects.filter(name=kubeconfig_local_name).exists():
LOGGER.debug("Creating kubeconfig Service Connection") status.messages.append("Creating kubeconfig Service Connection")
with kubeconfig_path.open("r", encoding="utf8") as _kubeconfig: with kubeconfig_path.open("r", encoding="utf8") as _kubeconfig:
KubernetesServiceConnection.objects.create( KubernetesServiceConnection.objects.create(
name=kubeconfig_local_name, name=kubeconfig_local_name,
@ -269,11 +271,12 @@ def outpost_local_connection():
unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path
socket = Path(unix_socket_path) socket = Path(unix_socket_path)
if socket.exists() and access(socket, R_OK): if socket.exists() and access(socket, R_OK):
LOGGER.info("Detected local docker socket") status.messages.append("Detected local docker socket")
if len(DockerServiceConnection.objects.filter(local=True)) == 0: if len(DockerServiceConnection.objects.filter(local=True)) == 0:
LOGGER.debug("Created Service Connection for docker") status.messages.append("Created Service Connection for docker")
DockerServiceConnection.objects.create( DockerServiceConnection.objects.create(
name="Local Docker connection", name="Local Docker connection",
local=True, local=True,
url=unix_socket_path, url=unix_socket_path,
) )
self.set_status(status)

View File

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

View File

@ -7,11 +7,6 @@ GAUGE_POLICIES_CACHED = Gauge(
"authentik_policies_cached", "authentik_policies_cached",
"Cached Policies", "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( HIST_POLICIES_EXECUTION_TIME = Histogram(
"authentik_policies_execution_time", "authentik_policies_execution_time",

View File

@ -10,7 +10,6 @@ from sentry_sdk.tracing import Span
from structlog.stdlib import BoundLogger, get_logger from structlog.stdlib import BoundLogger, get_logger
from authentik.core.models import User from authentik.core.models import User
from authentik.policies.apps import HIST_POLICIES_BUILD_TIME
from authentik.policies.exceptions import PolicyEngineException from authentik.policies.exceptions import PolicyEngineException
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel, PolicyEngineMode from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel, PolicyEngineMode
from authentik.policies.process import PolicyProcess, cache_key from authentik.policies.process import PolicyProcess, cache_key
@ -86,10 +85,6 @@ class PolicyEngine:
op="authentik.policy.engine.build", op="authentik.policy.engine.build",
description=self.__pbm, description=self.__pbm,
) as span, ) 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: Span
span.set_data("pbm", self.__pbm) 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) policy_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
name = models.TextField() name = models.TextField(unique=True)
execution_logging = models.BooleanField( execution_logging = models.BooleanField(
default=False, default=False,

View File

@ -2,7 +2,8 @@
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase 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.dummy.models import DummyPolicy
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.exceptions import PolicyEngineException from authentik.policies.exceptions import PolicyEngineException
@ -17,11 +18,17 @@ class TestPolicyEngine(TestCase):
def setUp(self): def setUp(self):
clear_policy_cache() clear_policy_cache()
self.user = User.objects.create_user(username="policyuser") self.user = create_test_admin_user()
self.policy_false = DummyPolicy.objects.create(result=False, wait_min=0, wait_max=1) self.policy_false = DummyPolicy.objects.create(
self.policy_true = DummyPolicy.objects.create(result=True, wait_min=0, wait_max=1) name=generate_id(), result=False, 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.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): def test_engine_empty(self):
"""Ensure empty policy list passes""" """Ensure empty policy list passes"""

View File

@ -26,6 +26,7 @@ class LDAPProviderSerializer(ProviderSerializer):
"search_mode", "search_mode",
"bind_mode", "bind_mode",
] ]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
class LDAPProviderViewSet(UsedByMixin, ModelViewSet): class LDAPProviderViewSet(UsedByMixin, ModelViewSet):

View File

@ -1,5 +1,6 @@
"""OAuth2Provider API Views""" """OAuth2Provider API Views"""
from django.urls import reverse from django.urls import reverse
from django.utils import timezone
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField from rest_framework.fields import CharField
@ -38,6 +39,7 @@ class OAuth2ProviderSerializer(ProviderSerializer):
"issuer_mode", "issuer_mode",
"jwks_sources", "jwks_sources",
] ]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
class OAuth2ProviderSetupURLs(PassiveSerializer): class OAuth2ProviderSetupURLs(PassiveSerializer):
@ -153,6 +155,7 @@ class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
user=request.user, user=request.user,
provider=provider, provider=provider,
_scope=" ".join(scope_names), _scope=" ".join(scope_names),
auth_time=timezone.now(),
), ),
request, request,
) )

View File

@ -141,15 +141,20 @@ class AuthorizeError(OAuth2Error):
), ),
} }
# pylint: disable=too-many-arguments
def __init__( def __init__(
self, self,
redirect_uri: str, redirect_uri: str,
error: str, error: str,
grant_type: str, grant_type: str,
state: str, state: str,
description: Optional[str] = None,
): ):
super().__init__() super().__init__()
self.error = error self.error = error
if description:
self.description = description
else:
self.description = self.errors[error] self.description = self.errors[error]
self.redirect_uri = redirect_uri self.redirect_uri = redirect_uri
self.grant_type = grant_type self.grant_type = grant_type
@ -169,10 +174,12 @@ class AuthorizeError(OAuth2Error):
# See: # See:
# http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError # 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 = ( uri = (
f"{self.redirect_uri}{hash_or_question}error=" f"{self.redirect_uri}{fragment_or_query}error="
f"{self.error}&error_description={description}" f"{self.error}&error_description={description}"
) )

View File

@ -110,12 +110,11 @@ class IDToken:
# Convert datetimes into timestamps. # Convert datetimes into timestamps.
now = timezone.now() now = timezone.now()
id_token.iat = int(now.timestamp()) 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 # We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
auth_event = get_login_event(request) auth_event = get_login_event(request)
if auth_event: if auth_event:
auth_time = auth_event.created
id_token.auth_time = int(auth_time.timestamp())
# Also check which method was used for authentication # Also check which method was used for authentication
method = auth_event.context.get(PLAN_CONTEXT_METHOD, "") method = auth_event.context.get(PLAN_CONTEXT_METHOD, "")
method_args = auth_event.context.get(PLAN_CONTEXT_METHOD_ARGS, {}) 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]: def get_issuer(self, request: HttpRequest) -> Optional[str]:
"""Get issuer, based on request""" """Get issuer, based on request"""
if self.issuer_mode == IssuerMode.GLOBAL: if self.issuer_mode == IssuerMode.GLOBAL:
return request.build_absolute_uri("/") return request.build_absolute_uri(reverse("authentik_core:root-redirect"))
try: try:
url = reverse( url = reverse(
"authentik_providers_oauth2:provider-root", "authentik_providers_oauth2:provider-root",
@ -282,6 +282,7 @@ class BaseGrantModel(models.Model):
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE) user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
revoked = models.BooleanField(default=False) revoked = models.BooleanField(default=False)
_scope = models.TextField(default="", verbose_name=_("Scopes")) _scope = models.TextField(default="", verbose_name=_("Scopes"))
auth_time = models.DateTimeField(verbose_name="Authentication time")
@property @property
def scope(self) -> list[str]: def scope(self) -> list[str]:

View File

@ -204,6 +204,7 @@ class TestAuthorize(OAuthTestCase):
"redirect_uri": "http://local.invalid/Foo", "redirect_uri": "http://local.invalid/Foo",
"scope": "openid", "scope": "openid",
"state": "foo", "state": "foo",
"nonce": generate_id(),
}, },
) )
self.assertEqual( self.assertEqual(
@ -325,6 +326,7 @@ class TestAuthorize(OAuthTestCase):
"state": state, "state": state,
"scope": "openid", "scope": "openid",
"redirect_uri": "http://localhost", "redirect_uri": "http://localhost",
"nonce": generate_id(),
}, },
) )
response = self.client.get( response = self.client.get(
@ -353,6 +355,62 @@ class TestAuthorize(OAuthTestCase):
delta=5, delta=5,
) )
def test_full_fragment_code(self):
"""Test full authorization"""
flow = create_test_flow()
provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
client_secret=generate_key(),
authorization_flow=flow,
redirect_uris="http://localhost",
signing_key=self.keypair,
)
Application.objects.create(name="app", slug="app", provider=provider)
state = generate_id()
user = create_test_admin_user()
self.client.force_login(user)
with patch(
"authentik.providers.oauth2.id_token.get_login_event",
MagicMock(
return_value=Event(
action=EventAction.LOGIN,
context={PLAN_CONTEXT_METHOD: "password"},
created=now(),
)
),
):
# Step 1, initiate params and get redirect to flow
self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
"response_mode": "fragment",
"client_id": "test",
"state": state,
"scope": "openid",
"redirect_uri": "http://localhost",
"nonce": generate_id(),
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"type": ChallengeTypes.REDIRECT.value,
"to": (f"http://localhost#code={code.code}" f"&state={state}"),
},
)
self.assertAlmostEqual(
code.expires.timestamp() - now().timestamp(),
timedelta_from_string(provider.access_code_validity).total_seconds(),
delta=5,
)
def test_full_form_post_id_token(self): def test_full_form_post_id_token(self):
"""Test full authorization (form_post response)""" """Test full authorization (form_post response)"""
flow = create_test_flow() flow = create_test_flow()
@ -378,6 +436,7 @@ class TestAuthorize(OAuthTestCase):
"state": state, "state": state,
"scope": "openid", "scope": "openid",
"redirect_uri": "http://localhost", "redirect_uri": "http://localhost",
"nonce": generate_id(),
}, },
) )
response = self.client.get( response = self.client.get(

View File

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

View File

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

View File

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

View File

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

View File

@ -146,9 +146,10 @@ def protected_resource_view(scopes: list[str]):
LOGGER.warning("Revoked token was used", access_token=access_token) LOGGER.warning("Revoked token was used", access_token=access_token)
Event.new( Event.new(
action=EventAction.SUSPICIOUS_REQUEST, action=EventAction.SUSPICIOUS_REQUEST,
message="Revoked refresh token was used", message="Revoked access token was used",
token=access_token, token=token,
).from_http(request) provider=token.provider,
).from_http(request, user=token.user)
raise BearerTokenError("invalid_token") raise BearerTokenError("invalid_token")
if not set(scopes).issubset(set(token.scope)): 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.core.models import Application
from authentik.events.models import Event, EventAction 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 ( from authentik.flows.challenge import (
PLAN_CONTEXT_TITLE, PLAN_CONTEXT_TITLE,
AutosubmitChallenge, AutosubmitChallenge,
ChallengeTypes, ChallengeTypes,
HttpChallengeResponse, HttpChallengeResponse,
) )
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.stage import StageView from authentik.flows.stage import StageView
@ -64,12 +65,11 @@ from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_PERMISSIONS, PLAN_CONTEXT_CONSENT_PERMISSIONS,
ConsentStageView, ConsentStageView,
) )
from authentik.stages.user_login.stage import USER_LOGIN_AUTHENTICATED
LOGGER = get_logger() LOGGER = get_logger()
PLAN_CONTEXT_PARAMS = "params" 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} ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN}
@ -158,13 +158,14 @@ class OAuthAuthorizationParams:
request=query_dict.get("request", None), request=query_dict.get("request", None),
max_age=int(max_age) if max_age else None, max_age=int(max_age) if max_age else None,
code_challenge=query_dict.get("code_challenge"), 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): def __post_init__(self):
try: self.provider: OAuth2Provider = OAuth2Provider.objects.filter(
self.provider: OAuth2Provider = OAuth2Provider.objects.get(client_id=self.client_id) client_id=self.client_id
except OAuth2Provider.DoesNotExist: ).first()
if not self.provider:
LOGGER.warning("Invalid client identifier", client_id=self.client_id) LOGGER.warning("Invalid client identifier", client_id=self.client_id)
raise ClientIdError(client_id=self.client_id) raise ClientIdError(client_id=self.client_id)
self.check_redirect_uri() self.check_redirect_uri()
@ -234,40 +235,54 @@ class OAuthAuthorizationParams:
def check_nonce(self): def check_nonce(self):
"""Nonce parameter validation.""" """Nonce parameter validation."""
# https://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation # nonce is required for all flows that return an id_token from the authorization endpoint,
# Nonce is only required for Implicit flows # see https://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthRequest or
if self.grant_type != GrantTypes.IMPLICIT: # 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 return
if not self.nonce: 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") LOGGER.warning("Missing nonce for OpenID Request")
raise AuthorizeError( raise AuthorizeError(self.redirect_uri, "invalid_request", self.grant_type, self.state)
self.redirect_uri, "invalid_request", self.grant_type, self.state
)
def check_code_challenge(self): def check_code_challenge(self):
"""PKCE validation of the transformation method.""" """PKCE validation of the transformation method."""
if self.code_challenge and self.code_challenge_method not in ["plain", "S256"]: 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: def create_code(self, request: HttpRequest) -> AuthorizationCode:
"""Create an AuthorizationCode object for the request""" """Create an AuthorizationCode object for the request"""
code = AuthorizationCode() auth_event = get_login_event(request)
code.user = request.user
code.provider = self.provider
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: if self.code_challenge and self.code_challenge_method:
code.code_challenge = self.code_challenge code.code_challenge = self.code_challenge
code.code_challenge_method = self.code_challenge_method 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 return code
@ -302,7 +317,6 @@ class AuthorizationFlowInitView(PolicyAccessView):
self.params.grant_type, self.params.grant_type,
self.params.state, self.params.state,
) )
error.to_event(redirect_uri=error.redirect_uri).from_http(self.request)
raise RequestValidationError(error.get_response(self.request)) raise RequestValidationError(error.get_response(self.request))
def resolve_provider_application(self): def resolve_provider_application(self):
@ -322,32 +336,45 @@ class AuthorizationFlowInitView(PolicyAccessView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Start FlowPLanner, return to flow executor shell""" """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 # After we've checked permissions, and the user has access, check if we need
# to re-authenticate the user # to re-authenticate the user
if self.params.max_age: if self.params.max_age:
current_age: timedelta = ( # Attempt to check via the session's login event if set, otherwise we can't
timezone.now() # check
- Event.objects.filter(action=EventAction.LOGIN, user=get_user(self.request.user)) login_time = login_event.created
.latest("created") current_age: timedelta = timezone.now() - login_time
.created
)
if current_age.total_seconds() > self.params.max_age: 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() return self.handle_no_permission()
# If prompt=login, we need to re-authenticate the user regardless # If prompt=login, we need to re-authenticate the user regardless
# 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 ( if (
PROMPT_LOGIN in self.params.prompt SESSION_KEY_LAST_LOGIN_UID not in self.request.session
and SESSION_KEY_NEEDS_LOGIN not in self.request.session or login_uid == self.request.session[SESSION_KEY_LAST_LOGIN_UID]
# 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 self.request.session[SESSION_KEY_LAST_LOGIN_UID] = login_uid
return self.handle_no_permission() return self.handle_no_permission()
scope_descriptions = UserInfoView().get_scope_descriptions(self.params.scope) scope_descriptions = UserInfoView().get_scope_descriptions(self.params.scope)
# Regardless, we start the planner and return to it # Regardless, we start the planner and return to it
planner = FlowPlanner(self.provider.authorization_flow) planner = FlowPlanner(self.provider.authorization_flow)
planner.allow_empty_flows = True planner.allow_empty_flows = True
try:
plan = planner.plan( plan = planner.plan(
self.request, self.request,
{ {
@ -361,6 +388,8 @@ class AuthorizationFlowInitView(PolicyAccessView):
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions, 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 # OpenID clients can specify a `prompt` parameter, and if its set to consent we
# need to inject a consent stage # need to inject a consent stage
if PROMPT_CONSENT in self.params.prompt: if PROMPT_CONSENT in self.params.prompt:
@ -485,6 +514,11 @@ class OAuthFulfillmentStage(StageView):
return urlunsplit(uri) return urlunsplit(uri)
if self.params.response_mode == ResponseMode.FRAGMENT: if self.params.response_mode == ResponseMode.FRAGMENT:
query_fragment = {}
if self.params.grant_type in [GrantTypes.AUTHORIZATION_CODE]:
query_fragment["code"] = code.code
query_fragment["state"] = [str(self.params.state) if self.params.state else ""]
else:
query_fragment = self.create_implicit_response(code) query_fragment = self.create_implicit_response(code)
uri = uri._replace( uri = uri._replace(
@ -518,6 +552,7 @@ class OAuthFulfillmentStage(StageView):
def create_implicit_response(self, code: Optional[AuthorizationCode]) -> dict: def create_implicit_response(self, code: Optional[AuthorizationCode]) -> dict:
"""Create implicit response's URL Fragment dictionary""" """Create implicit response's URL Fragment dictionary"""
query_fragment = {} query_fragment = {}
auth_event = get_login_event(self.request)
now = timezone.now() now = timezone.now()
access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity) access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity)
@ -526,6 +561,7 @@ class OAuthFulfillmentStage(StageView):
scope=self.params.scope, scope=self.params.scope,
expires=access_token_expiry, expires=access_token_expiry,
provider=self.provider, provider=self.provider,
auth_time=auth_event.created if auth_event else now,
) )
id_token = IDToken.new(self.provider, token, self.request) id_token = IDToken.new(self.provider, token, self.request)
@ -546,6 +582,8 @@ class OAuthFulfillmentStage(StageView):
ResponseTypes.CODE_TOKEN, ResponseTypes.CODE_TOKEN,
]: ]:
query_fragment["access_token"] = token.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. # Check if response_type must include id_token in the response.
if self.params.response_type in [ if self.params.response_type in [
@ -554,8 +592,6 @@ class OAuthFulfillmentStage(StageView):
ResponseTypes.CODE_ID_TOKEN, ResponseTypes.CODE_ID_TOKEN,
ResponseTypes.CODE_ID_TOKEN_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()) query_fragment["id_token"] = self.provider.encode(id_token.to_dict())
token._id_token = dumps(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.core.models import Application
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes 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.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
@ -57,6 +58,7 @@ def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]:
scope_descriptions = UserInfoView().get_scope_descriptions(token.scope) scope_descriptions = UserInfoView().get_scope_descriptions(token.scope)
planner = FlowPlanner(token.provider.authorization_flow) planner = FlowPlanner(token.provider.authorization_flow)
planner.allow_empty_flows = True planner.allow_empty_flows = True
try:
plan = planner.plan( plan = planner.plan(
request, request,
{ {
@ -70,6 +72,9 @@ def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]:
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions, PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
}, },
) )
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
return None
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage)) plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
request.session[SESSION_KEY_PLAN] = plan request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_with_qs(
@ -97,7 +102,11 @@ class DeviceEntryView(View):
# Regardless, we start the planner and return to it # Regardless, we start the planner and return to it
planner = FlowPlanner(device_flow) planner = FlowPlanner(device_flow)
planner.allow_empty_flows = True planner.allow_empty_flows = True
try:
plan = planner.plan(self.request) 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)) plan.append_stage(in_memory_stage(OAuthDeviceCodeStage))
self.request.session[SESSION_KEY_PLAN] = plan self.request.session[SESSION_KEY_PLAN] = plan

View File

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

View File

@ -95,6 +95,7 @@ class ProxyProviderSerializer(ProviderSerializer):
"refresh_token_validity", "refresh_token_validity",
"outpost_set", "outpost_set",
] ]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
class ProxyProviderViewSet(UsedByMixin, ModelViewSet): class ProxyProviderViewSet(UsedByMixin, ModelViewSet):

View File

@ -154,6 +154,7 @@ class SAMLProviderSerializer(ProviderSerializer):
"url_slo_post", "url_slo_post",
"url_slo_redirect", "url_slo_redirect",
] ]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
class SAMLMetadataSerializer(PassiveSerializer): class SAMLMetadataSerializer(PassiveSerializer):

View File

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

@ -10,8 +10,8 @@ from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.models import FlowDesignation from authentik.flows.models import FlowDesignation
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.tests.test_metadata import load_fixture
class TestSAMLProviderAPI(APITestCase): class TestSAMLProviderAPI(APITestCase):

View File

@ -1,6 +1,4 @@
"""Test Service-Provider Metadata Parser""" """Test Service-Provider Metadata Parser"""
from pathlib import Path
import xmlsec import xmlsec
from defusedxml.lxml import fromstring from defusedxml.lxml import fromstring
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
@ -9,6 +7,7 @@ from lxml import etree # nosec
from authentik.core.models import Application from authentik.core.models import Application
from authentik.core.tests.utils import create_test_cert, create_test_flow from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
from authentik.lib.xml import lxml_from_string from authentik.lib.xml import lxml_from_string
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.metadata import MetadataProcessor from authentik.providers.saml.processors.metadata import MetadataProcessor
@ -16,12 +15,6 @@ from authentik.providers.saml.processors.metadata_parser import ServiceProviderM
from authentik.sources.saml.processors.constants import NS_MAP from authentik.sources.saml.processors.constants import NS_MAP
def load_fixture(path: str, **kwargs) -> str:
"""Load fixture"""
with open(Path(__file__).resolve().parent / Path(path), "r", encoding="utf-8") as _fixture:
return _fixture.read()
class TestServiceProviderMetadataParser(TestCase): class TestServiceProviderMetadataParser(TestCase):
"""Test ServiceProviderMetadataParser parsing and creation of SAML Provider""" """Test ServiceProviderMetadataParser parsing and creation of SAML Provider"""
@ -59,7 +52,7 @@ class TestServiceProviderMetadataParser(TestCase):
request = self.factory.get("/") request = self.factory.get("/")
metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor()) 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)) self.assertTrue(schema.validate(metadata))
def test_simple(self): def test_simple(self):

View File

@ -46,7 +46,7 @@ class TestSchema(TestCase):
metadata = lxml_from_string(request) 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)) self.assertTrue(schema.validate(metadata))
def test_response_schema(self): def test_response_schema(self):
@ -67,5 +67,5 @@ class TestSchema(TestCase):
metadata = lxml_from_string(response) 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)) self.assertTrue(schema.validate(metadata))

View File

@ -1,7 +1,7 @@
"""authentik SAML IDP Views""" """authentik SAML IDP Views"""
from typing import Optional 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.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _ 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.core.models import Application
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST
@ -60,6 +61,7 @@ class SAMLSSOView(PolicyAccessView):
# Regardless, we start the planner and return to it # Regardless, we start the planner and return to it
planner = FlowPlanner(self.provider.authorization_flow) planner = FlowPlanner(self.provider.authorization_flow)
planner.allow_empty_flows = True planner.allow_empty_flows = True
try:
plan = planner.plan( plan = planner.plan(
request, request,
{ {
@ -70,6 +72,8 @@ class SAMLSSOView(PolicyAccessView):
PLAN_CONTEXT_CONSENT_PERMISSIONS: [], PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
}, },
) )
except FlowNonApplicableException:
raise Http404
plan.append_stage(in_memory_stage(SAMLFlowFinalView)) plan.append_stage(in_memory_stage(SAMLFlowFinalView))
request.session[SESSION_KEY_PLAN] = plan request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( 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)

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