Compare commits

...

216 Commits

Author SHA1 Message Date
435d126a1c release: 2022.8.1 2022-08-16 16:23:36 +02:00
ef5407be33 tests/e2e: fix blueprint paths
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-16 16:12:21 +02:00
e8b30b75d2 root: override blueprints_dir for testing
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-16 15:50:58 +02:00
2c09ac4fd6 root: fix dockerfile
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-16 14:32:45 +02:00
e9c1276634 blueprints: use relative path in @apply_blueprint
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-16 14:20:45 +02:00
71d6304407 website: update 2022.8 release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-16 13:47:48 +02:00
6000a33a8e *: fix type annotations for serializer model
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-16 13:23:22 +02:00
54eeb7add6 web/user: fix user source settings not updating correctly after deletion
also optimise the amount of API requests sent

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

#3411
2022-08-16 11:43:13 +02:00
d7e8ca1c8f ci: don't error when we cant add a PR comment
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-16 11:02:58 +02:00
740f4c7d56 web: bump @typescript-eslint/parser from 5.33.0 to 5.33.1 in /web (#3424) 2022-08-16 10:52:17 +02:00
088eb716fc web: bump @typescript-eslint/eslint-plugin from 5.33.0 to 5.33.1 in /web (#3425) 2022-08-16 10:50:44 +02:00
fb08e1db2b web/flows: only show permission ids if we have to, hide them if permission list has permissions with description
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-15 16:42:54 +02:00
f1b167c134 web: bump eslint from 8.21.0 to 8.22.0 in /web (#3419) 2022-08-15 16:07:52 +02:00
36d0817579 web: bump rollup from 2.77.3 to 2.78.0 in /web (#3420) 2022-08-15 16:07:44 +02:00
d4c1ef36f9 web: bump @sentry/browser from 7.9.0 to 7.10.0 in /web (#3402) 2022-08-12 14:19:52 +02:00
102b191e94 web: bump rollup from 2.77.2 to 2.77.3 in /web (#3407) 2022-08-12 14:15:41 +02:00
c735267373 web: bump lit from 2.2.8 to 2.3.0 in /web (#3408) 2022-08-12 14:15:32 +02:00
55b5e9834d core: bump sentry-sdk from 1.9.3 to 1.9.4 (#3409) 2022-08-12 14:15:22 +02:00
4386ffb8d7 web: bump @sentry/tracing from 7.9.0 to 7.10.0 in /web (#3403) 2022-08-11 12:50:38 +02:00
ca2b1a9c3e core: bump twilio from 7.12.0 to 7.12.1 (#3404) 2022-08-11 12:50:25 +02:00
22d33bb796 core: bump selenium from 4.3.0 to 4.4.0 (#3400) 2022-08-10 08:28:35 +02:00
ab15846480 core: bump sentry-sdk from 1.9.2 to 1.9.3 (#3401) 2022-08-10 08:27:32 +02:00
61f4049e51 website: bump react-before-after-slider-component from 1.1.3 to 1.1.4 in /website (#3393)
website: bump react-before-after-slider-component in /website

Bumps [react-before-after-slider-component](https://github.com/smeleshkin/react-before-after-slider-component) from 1.1.3 to 1.1.4.
- [Release notes](https://github.com/smeleshkin/react-before-after-slider-component/releases)
- [Commits](https://github.com/smeleshkin/react-before-after-slider-component/compare/v.1.1.3...v.1.1.4)

---
updated-dependencies:
- dependency-name: react-before-after-slider-component
  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>
2022-08-09 11:30:06 +02:00
5ac4542f37 web: bump @typescript-eslint/eslint-plugin from 5.32.0 to 5.33.0 in /web (#3394)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.32.0 to 5.33.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.33.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>
2022-08-09 11:29:10 +02:00
0bba536187 web: bump @typescript-eslint/parser from 5.32.0 to 5.33.0 in /web (#3395) 2022-08-09 09:32:26 +02:00
e48d5a5fda core: bump goauthentik.io/api/v3 from 3.2022073.7 to 3.2022073.8 (#3396) 2022-08-09 09:32:11 +02:00
4c9878313c sources/oauth: correctly concatenate URLs to allow custom parameters to be included
closes #3374

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-08 21:17:32 +02:00
6356ddd9f3 internal: replace ioutils
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-08 21:00:45 +02:00
7ea0b4b9e4 web: Update Web API Client version (#3391)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-08-08 20:42:26 +02:00
a520a60c82 blueprints: lowercase system blueprints' models
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-08 20:38:42 +02:00
54c16129ea stages/authenticator_duo: revamp duo enroll status API
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#3288
2022-08-08 20:38:06 +02:00
2858682866 web: bump @sentry/browser from 7.8.1 to 7.9.0 in /web (#3381) 2022-08-08 13:04:46 +02:00
b1f7dfaeb4 website: bump postcss from 8.4.14 to 8.4.16 in /website (#3380) 2022-08-08 13:03:10 +02:00
eb27be222f web: bump @sentry/tracing from 7.8.1 to 7.9.0 in /web (#3382) 2022-08-08 13:03:00 +02:00
bcbeaa8382 web: bump @rollup/plugin-commonjs from 22.0.1 to 22.0.2 in /web (#3383) 2022-08-08 13:02:12 +02:00
d5cb95fedd core: bump coverage from 6.4.2 to 6.4.3 (#3384) 2022-08-08 13:02:02 +02:00
fda0da4659 core: bump sentry-sdk from 1.9.0 to 1.9.2 (#3385) 2022-08-08 13:01:50 +02:00
1c569c79f3 website: add more blueprint docs, 2022.8 release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-07 19:27:31 +02:00
872c18dddc blueprints: don't use example label, add more tags and tests for tags
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-07 19:27:03 +02:00
201bea6d30 internal: add X-authentik-logout signature to trigger logouts when URLs are not exposed
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-07 18:50:24 +02:00
7e3c21d77b web/flows: update flow background
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-07 15:34:04 +02:00
d1a02cbc0c tests/e2e: bump chrome version
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-07 14:50:08 +02:00
da666871dd stages/consent: simplify permission rendering
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-07 14:50:00 +02:00
2fa6cf855d stages/consent: simplify logic, correctly update existing consent
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-07 14:38:40 +02:00
3b86144ae5 stages/*: use stage-bound logger when possible
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-07 13:41:53 +02:00
f01f10c5e5 providers/oauth2: don't separate scopes by comma-space
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-07 13:15:12 +02:00
e1249d3760 providers/oauth2: fix scopes without descriptions not being saved in consent
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-07 13:02:47 +02:00
dcbf106daa blueprints: add !Context to lookup things from instance context
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-06 20:54:00 +02:00
0227ba90bb core: bump github.com/prometheus/client_golang from 1.12.2 to 1.13.0 (#3379)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.12.2 to 1.13.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.12.2...v1.13.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  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>
2022-08-06 13:08:59 +02:00
b095d63b4d core: bump goauthentik.io/api/v3 from 3.2022073.6 to 3.2022073.7 (#3378)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2022073.6 to 3.2022073.7.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2022073.6...v3.2022073.7)

---
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>
2022-08-06 12:57:30 +02:00
20648808bc web: Update Web API Client version (#3377) 2022-08-06 01:02:05 +02:00
89fef0ae72 blueprints: docs (#3376)
* further blueprint cleanup

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

* more

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

* make group users and parent optional

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

* fix api client usage

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-06 00:52:12 +02:00
85640d402f internal: fix race conditions when accessing settings before bootstrap
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-06 00:24:55 +02:00
d5703dce39 internal: fix outposts not reacting to signals while starting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-05 18:13:09 +02:00
ec42d378ab blueprints/cleanup (#3369) 2022-08-05 08:39:00 +02:00
2a5a975d9a core: bump python from 3.10.5-slim-bullseye to 3.10.6-slim-bullseye (#3375) 2022-08-05 08:37:14 +02:00
a398e2470b web: bump chart.js from 3.9.0 to 3.9.1 in /web (#3371)
Bumps [chart.js](https://github.com/chartjs/Chart.js) from 3.9.0 to 3.9.1.
- [Release notes](https://github.com/chartjs/Chart.js/releases)
- [Commits](https://github.com/chartjs/Chart.js/compare/v3.9.0...v3.9.1)

---
updated-dependencies:
- dependency-name: chart.js
  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>
2022-08-04 10:10:19 +02:00
2ce8e18bab internal: centralise config for listeners to use same config system everywhere (#3367)
* centralise config for listeners to use same config system everywhere

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

#3360

* add docs

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-03 21:33:27 +02:00
9a9c826c0b core: bump django from 4.0.6 to 4.1 (#3368)
* core: bump django from 4.0.6 to 4.1

Bumps [django](https://github.com/django/django) from 4.0.6 to 4.1.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/4.0.6...4.1)

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

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

* fix tests

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-03 15:33:58 +02:00
612587dd34 core: bump golang from 1.18.5-bullseye to 1.19.0-bullseye (#3364) 2022-08-03 09:10:23 +02:00
506722587d web: bump chart.js from 3.8.2 to 3.9.0 in /web (#3365) 2022-08-03 09:09:53 +02:00
c158604424 core: bump goauthentik.io/api/v3 from 3.2022073.5 to 3.2022073.6 (#3366) 2022-08-03 09:09:35 +02:00
4af6f29025 web: Update Web API Client version (#3357) 2022-08-03 00:15:32 +02:00
d1004e3798 blueprints: webui (#3356) 2022-08-03 00:05:49 +02:00
20aeed139d web: bump @sentry/browser from 7.8.0 to 7.8.1 in /web (#3349)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 7.8.0 to 7.8.1.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.8.0...7.8.1)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  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>
2022-08-02 09:53:46 +02:00
b2dbc87a4f web: bump @babel/preset-env from 7.18.9 to 7.18.10 in /web (#3347)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.18.9 to 7.18.10.
- [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.18.10/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  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>
2022-08-02 09:53:35 +02:00
11bd7c72e0 website: bump @docusaurus/plugin-client-redirects from 2.0.0-rc.1 to 2.0.1 in /website (#3346)
website: bump @docusaurus/plugin-client-redirects in /website

Bumps [@docusaurus/plugin-client-redirects](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-plugin-client-redirects) from 2.0.0-rc.1 to 2.0.1.
- [Release notes](https://github.com/facebook/docusaurus/releases)
- [Changelog](https://github.com/facebook/docusaurus/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/docusaurus/commits/v2.0.1/packages/docusaurus-plugin-client-redirects)

---
updated-dependencies:
- dependency-name: "@docusaurus/plugin-client-redirects"
  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>
2022-08-02 09:53:27 +02:00
896cf7594e website: bump @docusaurus/preset-classic from 2.0.0-rc.1 to 2.0.1 in /website (#3345)
website: bump @docusaurus/preset-classic in /website

Bumps [@docusaurus/preset-classic](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-preset-classic) from 2.0.0-rc.1 to 2.0.1.
- [Release notes](https://github.com/facebook/docusaurus/releases)
- [Changelog](https://github.com/facebook/docusaurus/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/docusaurus/commits/v2.0.1/packages/docusaurus-preset-classic)

---
updated-dependencies:
- dependency-name: "@docusaurus/preset-classic"
  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>
2022-08-02 09:51:56 +02:00
457eeb10ab web: bump @babel/plugin-transform-runtime from 7.18.9 to 7.18.10 in /web (#3350)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.18.9 to 7.18.10.
- [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.18.10/packages/babel-plugin-transform-runtime)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-runtime"
  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>
2022-08-02 09:51:40 +02:00
0d94ffb91b web: bump eslint from 8.20.0 to 8.21.0 in /web (#3348)
Bumps [eslint](https://github.com/eslint/eslint) from 8.20.0 to 8.21.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.20.0...v8.21.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>
2022-08-02 09:51:31 +02:00
bd4fccb97b web: bump @typescript-eslint/parser from 5.31.0 to 5.32.0 in /web (#3352)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.31.0 to 5.32.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.32.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>
2022-08-02 09:51:22 +02:00
0cfae088f5 web: bump @babel/core from 7.18.9 to 7.18.10 in /web (#3351)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.18.9 to 7.18.10.
- [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.18.10/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>
2022-08-02 09:51:05 +02:00
b4e7663209 web: bump @sentry/tracing from 7.8.0 to 7.8.1 in /web (#3344)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 7.8.0 to 7.8.1.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.8.0...7.8.1)

---
updated-dependencies:
- dependency-name: "@sentry/tracing"
  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>
2022-08-02 09:50:49 +02:00
bdca49b265 core: bump golang from 1.18.4-bullseye to 1.18.5-bullseye (#3343)
Bumps golang from 1.18.4-bullseye to 1.18.5-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>
2022-08-02 09:50:36 +02:00
c22954b6a4 web: bump @typescript-eslint/eslint-plugin from 5.31.0 to 5.32.0 in /web (#3353)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.31.0 to 5.32.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.32.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>
2022-08-02 09:50:07 +02:00
dfe97625f0 web: bump @babel/plugin-proposal-decorators from 7.18.9 to 7.18.10 in /web (#3354)
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.18.9 to 7.18.10.
- [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.18.10/packages/babel-plugin-proposal-decorators)

---
updated-dependencies:
- dependency-name: "@babel/plugin-proposal-decorators"
  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>
2022-08-02 09:49:55 +02:00
74bc0a6301 core: bump goauthentik.io/api/v3 from 3.2022073.4 to 3.2022073.5 (#3355)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2022073.4 to 3.2022073.5.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2022073.4...v3.2022073.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>
2022-08-02 09:49:45 +02:00
2bd29e2fdd *: improve error handling for startup tasks
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-01 23:31:47 +02:00
3cd0a782af blueprints: correctly load on fresh install
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-01 23:25:33 +02:00
d88ce7a43f web: Update Web API Client version (#3342)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-08-01 23:10:48 +02:00
a023eee9bf blueprints: migrate from managed (#3338)
* test all bundled blueprints

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

* fix empty title

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

* fix default blueprints

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

* add script to generate dev config

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

* migrate managed to blueprints

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

* add more to blueprint instance

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

* migrated away from ObjectManager

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

* fix lint errors

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

* migrate things

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

* migrate tests

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

* fix some tests

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

* fix a bit more

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

* fix more tests

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

* whops

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

* fix missing name

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

* *sigh*

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

* fix more tests

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

* add tasks

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

* scheduled

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

* run discovery on start

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

* oops this test should stay

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-01 23:05:58 +02:00
7a05c6faef stages/consent: fix error when requests with identical empty permissions
closes #3280

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-01 20:58:49 +02:00
6ab85f7f18 core: bump goauthentik.io/api/v3 from 3.2022073.2 to 3.2022073.4 (#3341) 2022-08-01 09:28:08 +02:00
d12907b81f web: Update Web API Client version (#3340)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-07-31 23:52:48 +02:00
553989d17f flows/stages/consent: fix for post requests (#3339)
add unique token to consent stage to ensure it is shown

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-31 23:47:40 +02:00
475019202b root: fix dockerfile for blueprints
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-31 18:10:21 +02:00
a9bca55b95 web: Update Web API Client version (#3337)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-07-31 17:18:00 +02:00
89c84f10d0 blueprints: v1 (#1573)
* managed: move flowexporter to managed

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

* *: implement SerializerModel in all models

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

* managed: add initial api

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

* managed: start blueprint

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

* managed: spec

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

* version blueprint

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

* yep

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

* remove v2, improve v1

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

* start custom tag, more rebrand

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

* add default flows

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

* move blueprints out of website

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

* try new things

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

* add !lookup, fix web

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

* update and cleanup default

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

* fix tags in lists

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

* don't save field if its set to default value

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

* more flow cleanup

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

* format web

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

* fix missing serializer for sms

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

* ignore _set fields

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

* remove custom file extension

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

* migrate default flow to tenant

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

* include blueprints

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

* fix tests

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-31 17:11:44 +02:00
882250a85e flows: migrate flows to be yaml (#3335)
* flows: migrate flows to be yaml

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

* migrate flows to yaml

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-30 23:55:58 +02:00
db1dd196e0 lifecycle: optimise container lifecycle and process signals (#3332)
* add dumb-init, use exec in wrapper

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

* fix exec?

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-30 22:41:29 +02:00
d4b8dd7fcc ci: comment on PR with instructions on how to use branch (#3333)
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-30 21:22:27 +02:00
fcf4657833 providers/proxy: add is_superuser to ak_proxy object, only show full error when superuser
closes #3314

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-30 20:29:23 +02:00
393d7ec486 providers/proxy: no exposed urls (#3151)
* test any callback

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

* cleanup

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

* dont detect callback in per-server handler

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

* use full redirect uri with both path and query param

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

* update tests

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

* correctly route to embedded outpost for callback signature

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

* fix allowed redirects

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-30 17:51:01 +02:00
b7b5168910 sources/oauth: use mailcow full_name as username for mailcow source (#3299)
use mailcow full_name as username
2022-07-29 20:34:17 +00:00
1dcec17a58 sources/oauth: only send header authentication for OIDC source
closes #3327

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-29 18:20:44 +02:00
b41acebf5b providers/proxy: add caddy endpoint (#3330)
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-29 10:58:53 +02:00
e0478e1775 core: bump goauthentik.io/api/v3 from 3.2022073.1 to 3.2022073.2 (#3331)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2022073.1 to 3.2022073.2.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2022073.1...v3.2022073.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>
2022-07-29 10:35:23 +02:00
d6b1a22563 core: fix import order
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-29 00:18:42 +02:00
cada292e00 core: pre-hydrate config into templates to directly load correct assets
closes #3228

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-29 00:04:44 +02:00
d4af47f576 web: Update Web API Client version (#3329)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-07-28 23:59:04 +02:00
83eba36f8d core: add API Endpoint to get all MFA devices, add web ui to delete MFA devices of any user
closes #3237

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-28 23:50:25 +02:00
b82a142745 stages/authenticator_sms: use twilio SDK, improve docs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#3237
2022-07-28 22:17:59 +02:00
2a42c203b2 stages/authenticator_totp: remove single device per user limit
closes #3281

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-28 21:39:46 +02:00
ade2d4879c stages/authenticator_duo: fix imported Duo Device not having a name
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-28 21:20:32 +02:00
e14798dcdc core: import all models into shell
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-28 21:19:04 +02:00
e49050af19 web/flows: improve layout for TOTP QR stage
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-28 21:16:42 +02:00
0248755cda stages/authentiactor_validate: improve error handling for duo
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-28 21:11:58 +02:00
1f90359310 root: fix broken traceback logging
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-28 20:56:39 +02:00
008fc19f0d root: fix log fields being overwritten in celery task logs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-28 20:48:05 +02:00
5cd5b71b18 web: bump @sentry/tracing from 7.7.0 to 7.8.0 in /web (#3324) 2022-07-28 09:46:29 +02:00
253d98ba73 web: bump @rollup/plugin-typescript from 8.3.3 to 8.3.4 in /web (#3323) 2022-07-28 09:46:18 +02:00
3e2c04d029 web: bump rollup from 2.77.1 to 2.77.2 in /web (#3322) 2022-07-28 09:45:29 +02:00
32d3e33c83 web: bump @sentry/browser from 7.7.0 to 7.8.0 in /web (#3325) 2022-07-28 09:45:07 +02:00
282ebeb38b core: bump drf-spectacular from 0.22.1 to 0.23.1 (#3319)
Bumps [drf-spectacular](https://github.com/tfranzel/drf-spectacular) from 0.22.1 to 0.23.1.
- [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.22.1...0.23.1)

---
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>
2022-07-27 09:48:43 +02:00
81f4d86e6d core: bump github.com/go-ldap/ldap/v3 from 3.4.3 to 3.4.4 (#3318)
Bumps [github.com/go-ldap/ldap/v3](https://github.com/go-ldap/ldap) from 3.4.3 to 3.4.4.
- [Release notes](https://github.com/go-ldap/ldap/releases)
- [Commits](https://github.com/go-ldap/ldap/compare/v3.4.3...v3.4.4)

---
updated-dependencies:
- dependency-name: github.com/go-ldap/ldap/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>
2022-07-27 09:48:34 +02:00
bcb90997bb web: bump rollup from 2.77.0 to 2.77.1 in /web (#3317)
Bumps [rollup](https://github.com/rollup/rollup) from 2.77.0 to 2.77.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.77.0...v2.77.1)

---
updated-dependencies:
- dependency-name: rollup
  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>
2022-07-27 09:48:23 +02:00
277df4f04f stages/prompt: fix tests for file field
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-27 09:48:11 +02:00
de26c65fa0 core: add attributes. avatar method to allow custom uploaded avatars
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#2631
2022-07-26 21:42:41 +02:00
55739ee982 internal: add additional error handling in config loader
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-26 11:48:57 +02:00
10b48b27b0 internal: walk config in go, check, parse and load from scheme like in python
closes #2719

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-26 11:33:37 +02:00
d08856c1fe web: bump @typescript-eslint/eslint-plugin from 5.30.7 to 5.31.0 in /web (#3311)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.30.7 to 5.31.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.31.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>
2022-07-26 09:47:28 +02:00
0d108b043d web: bump @fortawesome/fontawesome-free from 6.1.1 to 6.1.2 in /web (#3310)
Bumps [@fortawesome/fontawesome-free](https://github.com/FortAwesome/Font-Awesome) from 6.1.1 to 6.1.2.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.1.1...6.1.2)

---
updated-dependencies:
- dependency-name: "@fortawesome/fontawesome-free"
  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>
2022-07-26 09:41:33 +02:00
a37245b7c8 web: bump @typescript-eslint/parser from 5.30.7 to 5.31.0 in /web (#3312)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.30.7 to 5.31.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.31.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>
2022-07-26 09:41:24 +02:00
19c4ebdda9 core: bump urllib3 from 1.26.9 to 1.26.11 (#3313)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.9 to 1.26.11.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/1.26.11/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.9...1.26.11)

---
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>
2022-07-26 09:41:03 +02:00
34d4b71f59 web: bump chart.js from 3.8.0 to 3.8.2 in /web (#3307)
Bumps [chart.js](https://github.com/chartjs/Chart.js) from 3.8.0 to 3.8.2.
- [Release notes](https://github.com/chartjs/Chart.js/releases)
- [Commits](https://github.com/chartjs/Chart.js/compare/v3.8.0...v3.8.2)

---
updated-dependencies:
- dependency-name: chart.js
  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>
2022-07-25 09:22:45 +02:00
bd8794f646 core: bump structlog from 21.5.0 to 22.1.0 (#3294)
* core: bump structlog from 21.5.0 to 22.1.0

Bumps [structlog](https://github.com/hynek/structlog) from 21.5.0 to 22.1.0.
- [Release notes](https://github.com/hynek/structlog/releases)
- [Changelog](https://github.com/hynek/structlog/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/structlog/compare/21.5.0...22.1.0)

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

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

* migrate threaedlocal to contextvars

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-23 22:40:56 +02:00
e0adcd3277 web: bump @codemirror/lang-javascript from 6.0.1 to 6.0.2 in /web (#3300)
Bumps [@codemirror/lang-javascript](https://github.com/codemirror/lang-javascript) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/codemirror/lang-javascript/releases)
- [Changelog](https://github.com/codemirror/lang-javascript/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/lang-javascript/compare/6.0.1...6.0.2)

---
updated-dependencies:
- dependency-name: "@codemirror/lang-javascript"
  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>
2022-07-22 11:15:40 +02:00
e2fe615a31 web: bump lit from 2.2.7 to 2.2.8 in /web (#3301)
Bumps [lit](https://github.com/lit/lit/tree/HEAD/packages/lit) from 2.2.7 to 2.2.8.
- [Release notes](https://github.com/lit/lit/releases)
- [Changelog](https://github.com/lit/lit/blob/main/packages/lit/CHANGELOG.md)
- [Commits](https://github.com/lit/lit/commits/lit@2.2.8/packages/lit)

---
updated-dependencies:
- dependency-name: lit
  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>
2022-07-22 10:52:56 +02:00
0d2536fdfd web: bump @codemirror/lang-python from 6.0.0 to 6.0.1 in /web (#3302)
Bumps [@codemirror/lang-python](https://github.com/codemirror/lang-python) from 6.0.0 to 6.0.1.
- [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.0.0...6.0.1)

---
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>
2022-07-22 10:52:49 +02:00
c5813204d5 core: bump sentry-sdk from 1.7.2 to 1.8.0 (#3303)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 1.7.2 to 1.8.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.7.2...1.8.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>
2022-07-22 10:52:40 +02:00
5064c39a94 core: bump goauthentik.io/api/v3 from 3.2022072.1 to 3.2022073.1 (#3295) 2022-07-21 08:45:41 +02:00
1880f98fa1 sources/oauth: fix typo
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-20 19:10:26 +02:00
d3c360d9e5 root: remove 2022.5 from supported list
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-20 18:49:38 +02:00
388c4b9653 web: Update Web API Client version (#3293) 2022-07-20 18:43:24 +02:00
87f1cf17c4 Merge branch 'version-2022.7' 2022-07-20 18:39:48 +02:00
dae6493a3e release: 2022.7.3 2022-07-20 09:37:43 +02:00
fc0275314d website: bump terser from 5.10.0 to 5.14.2 in /website (#3289)
Bumps [terser](https://github.com/terser/terser) from 5.10.0 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 09:37:22 +02:00
c6fa19155b web: bump terser from 5.7.1 to 5.14.2 in /web (#3285)
Bumps [terser](https://github.com/terser/terser) from 5.7.1 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 09:32:26 +02:00
2fa0191d65 core: bump github.com/sirupsen/logrus from 1.8.1 to 1.9.0 (#3286)
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.8.1 to 1.9.0.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.8.1...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/sirupsen/logrus
  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>
2022-07-20 09:32:16 +02:00
ad07984158 website/docs: prepare 2022.7.3
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-19 21:40:56 +02:00
f909b86338 stages/consent: fix permimssions for consent API (allow owner to delete) 2022-07-19 16:41:34 +00:00
327df6529b sources/oauth: use oidc preferred_username if set, otherwise nickname 2022-07-19 16:41:10 +00:00
658dc63c4c lifecycle: revert waiting for lock, launch managed reconcile on app import
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-19 12:06:57 +02:00
4edec5f666 lifecycle: connect to database first
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-19 10:54:56 +02:00
d150a0c135 web: bump @typescript-eslint/eslint-plugin from 5.30.6 to 5.30.7 in /web (#3278) 2022-07-19 08:48:26 +02:00
d4242781a0 web: bump @babel/plugin-transform-runtime from 7.18.6 to 7.18.9 in /web (#3277) 2022-07-19 08:48:09 +02:00
7369ca0b25 web: bump @babel/plugin-proposal-decorators from 7.18.6 to 7.18.9 in /web (#3273) 2022-07-19 08:46:24 +02:00
561f427cc5 web: bump @typescript-eslint/parser from 5.30.6 to 5.30.7 in /web (#3274) 2022-07-19 08:46:14 +02:00
8049ab703a web: bump @trivago/prettier-plugin-sort-imports from 3.2.0 to 3.3.0 in /web (#3275) 2022-07-19 08:46:02 +02:00
9c2a97263a web: bump @babel/core from 7.18.6 to 7.18.9 in /web (#3276) 2022-07-19 08:45:35 +02:00
345504c1a4 web: bump @babel/preset-env from 7.18.6 to 7.18.9 in /web (#3279) 2022-07-19 08:45:00 +02:00
549f6f2077 providers/oauth2: correctly log authenticated user for OAuth views using protected_resource_view
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-18 22:20:09 +02:00
35c6decc75 web: bump @sentry/tracing from 7.6.0 to 7.7.0 in /web (#3266)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 7.6.0 to 7.7.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.6.0...7.7.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>
2022-07-18 10:49:46 +02:00
b3abeb78ff website: bump react-toggle from 4.1.2 to 4.1.3 in /website (#3263)
Bumps [react-toggle](https://github.com/aaronshaf/react-toggle) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/aaronshaf/react-toggle/releases)
- [Changelog](https://github.com/aaronshaf/react-toggle/blob/master/CHANGELOG.md)
- [Commits](https://github.com/aaronshaf/react-toggle/commits)

---
updated-dependencies:
- dependency-name: react-toggle
  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>
2022-07-18 10:48:59 +02:00
0562a1ad42 web: bump @sentry/browser from 7.6.0 to 7.7.0 in /web (#3264)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 7.6.0 to 7.7.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.6.0...7.7.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>
2022-07-18 10:48:50 +02:00
febb0920fd web: bump rollup from 2.76.0 to 2.77.0 in /web (#3265)
Bumps [rollup](https://github.com/rollup/rollup) from 2.76.0 to 2.77.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.76.0...v2.77.0)

---
updated-dependencies:
- dependency-name: rollup
  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>
2022-07-18 10:48:40 +02:00
549662beb0 web: bump eslint from 8.19.0 to 8.20.0 in /web (#3267)
Bumps [eslint](https://github.com/eslint/eslint) from 8.19.0 to 8.20.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.19.0...v8.20.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>
2022-07-18 10:48:28 +02:00
1ea4440c5d core: bump sentry-sdk from 1.7.1 to 1.7.2 (#3268)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 1.7.1 to 1.7.2.
- [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.7.1...1.7.2)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-18 10:48:19 +02:00
787abdff5b core: bump pylint from 2.14.4 to 2.14.5 (#3269)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.14.4 to 2.14.5.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.14.4...v2.14.5)

---
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>
2022-07-18 09:29:57 +02:00
2237807241 core: bump github.com/go-openapi/strfmt from 0.21.2 to 0.21.3 (#3270)
Bumps [github.com/go-openapi/strfmt](https://github.com/go-openapi/strfmt) from 0.21.2 to 0.21.3.
- [Release notes](https://github.com/go-openapi/strfmt/releases)
- [Commits](https://github.com/go-openapi/strfmt/compare/v0.21.2...v0.21.3)

---
updated-dependencies:
- dependency-name: github.com/go-openapi/strfmt
  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>
2022-07-18 09:29:43 +02:00
e9d9d658c4 lifecycle: make worker wait for migrations to be done (#3254)
* lifecycle: make worker wait for migrations to be done

* retry managed reconcile task
2022-07-15 19:44:45 +02:00
e704092d19 website: bump @docusaurus/plugin-client-redirects from 2.0.0-beta.22 to 2.0.0-rc.1 in /website (#3260)
website: bump @docusaurus/plugin-client-redirects in /website

Bumps [@docusaurus/plugin-client-redirects](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-plugin-client-redirects) from 2.0.0-beta.22 to 2.0.0-rc.1.
- [Release notes](https://github.com/facebook/docusaurus/releases)
- [Changelog](https://github.com/facebook/docusaurus/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/docusaurus/commits/v2.0.0-rc.1/packages/docusaurus-plugin-client-redirects)

---
updated-dependencies:
- dependency-name: "@docusaurus/plugin-client-redirects"
  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>
2022-07-15 09:16:08 +02:00
305f72c197 website: bump @docusaurus/preset-classic from 2.0.0-beta.22 to 2.0.0-rc.1 in /website (#3259) 2022-07-15 09:01:28 +02:00
fb6b6b4476 ci: bump actions/setup-node from 3.4.0 to 3.4.1 (#3261) 2022-07-15 09:00:44 +02:00
791cc74dbb core: bump golang from 1.18.3-bullseye to 1.18.4-bullseye (#3255)
Bumps golang from 1.18.3-bullseye to 1.18.4-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>
2022-07-14 10:17:48 +02:00
41f139589c core: bump webauthn from 1.5.2 to 1.6.0 (#3256)
Bumps [webauthn](https://github.com/duo-labs/py_webauthn) from 1.5.2 to 1.6.0.
- [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.5.2...v1.6.0)

---
updated-dependencies:
- dependency-name: webauthn
  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>
2022-07-14 10:16:14 +02:00
df24e3020b core: bump sentry-sdk from 1.7.0 to 1.7.1 (#3257) 2022-07-14 09:12:58 +02:00
e44c716cbe website/integrations: add note for rancher idp initiated
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-13 23:14:01 +02:00
d35302923d core: bump coverage from 6.4.1 to 6.4.2 (#3251)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 6.4.1 to 6.4.2.
- [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/6.4.1...6.4.2)

---
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>
2022-07-13 21:06:06 +02:00
4d928368bc core: bump channels-redis from 3.4.0 to 3.4.1 (#3252)
Bumps [channels-redis](https://github.com/django/channels_redis) from 3.4.0 to 3.4.1.
- [Release notes](https://github.com/django/channels_redis/releases)
- [Changelog](https://github.com/django/channels_redis/blob/main/CHANGELOG.txt)
- [Commits](https://github.com/django/channels_redis/compare/3.4.0...3.4.1)

---
updated-dependencies:
- dependency-name: channels-redis
  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>
2022-07-13 09:59:52 +02:00
c055d7a470 web: bump @typescript-eslint/parser from 5.30.5 to 5.30.6 in /web (#3244) 2022-07-12 09:22:46 +02:00
9e1b49e181 web: bump @typescript-eslint/eslint-plugin from 5.30.5 to 5.30.6 in /web (#3245) 2022-07-12 09:21:29 +02:00
db6a9ede1b ci: bump actions/setup-node from 3.3.0 to 3.4.0 (#3247) 2022-07-12 09:21:16 +02:00
86df0a448e core: bump sentry-sdk from 1.6.0 to 1.7.0 (#3246) 2022-07-12 09:20:42 +02:00
5ec052bd92 website/integrations: Node-Red integration with openidconnect (#3221)
* add Node-Red integration doc

* Node-Red Protocol settings + linting fixes
2022-07-11 10:59:57 +02:00
6f7984d05a website: bump @docusaurus/plugin-client-redirects from 2.0.0-beta.21 to 2.0.0-beta.22 in /website (#3239)
website: bump @docusaurus/plugin-client-redirects in /website

Bumps [@docusaurus/plugin-client-redirects](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-plugin-client-redirects) from 2.0.0-beta.21 to 2.0.0-beta.22.
- [Release notes](https://github.com/facebook/docusaurus/releases)
- [Changelog](https://github.com/facebook/docusaurus/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/docusaurus/commits/v2.0.0-beta.22/packages/docusaurus-plugin-client-redirects)

---
updated-dependencies:
- dependency-name: "@docusaurus/plugin-client-redirects"
  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>
2022-07-11 09:35:15 +02:00
f6d64d1d4b website: bump @docusaurus/preset-classic from 2.0.0-beta.21 to 2.0.0-beta.22 in /website (#3242)
website: bump @docusaurus/preset-classic in /website

Bumps [@docusaurus/preset-classic](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-preset-classic) from 2.0.0-beta.21 to 2.0.0-beta.22.
- [Release notes](https://github.com/facebook/docusaurus/releases)
- [Changelog](https://github.com/facebook/docusaurus/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/docusaurus/commits/v2.0.0-beta.22/packages/docusaurus-preset-classic)

---
updated-dependencies:
- dependency-name: "@docusaurus/preset-classic"
  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>
2022-07-11 09:34:12 +02:00
ef0c7a5a57 web: bump @sentry/browser from 7.5.1 to 7.6.0 in /web (#3238)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 7.5.1 to 7.6.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.5.1...7.6.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>
2022-07-11 09:33:52 +02:00
34dfbf8e9e web: bump @sentry/tracing from 7.5.1 to 7.6.0 in /web (#3240)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 7.5.1 to 7.6.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.5.1...7.6.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>
2022-07-11 09:31:41 +02:00
71d38e6fd0 web: bump rollup from 2.75.7 to 2.76.0 in /web (#3241) 2022-07-11 09:27:53 +02:00
9a9ba2560b core: delete expired models when filtering instead of excluding them
closes #3233

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-09 13:40:39 +02:00
2432e51970 web/elements: improve contrast for codemirror backgrounds
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-08 22:58:39 +02:00
47434cd62d stages/prompt: try to base64 decode file, fallback to keeping value as-is
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-08 22:45:31 +02:00
ff500b44a6 stages/prompt: force required to false when using readonlyfield
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-08 22:38:37 +02:00
4c14c7f3a4 web: bump @sentry/browser from 7.5.0 to 7.5.1 in /web (#3230) 2022-07-07 09:44:22 +02:00
019c4bf182 web: bump @sentry/tracing from 7.5.0 to 7.5.1 in /web (#3231) 2022-07-07 09:27:17 +02:00
2cbc291f04 core: bump goauthentik.io/api/v3 from 3.2022071.2 to 3.2022072.1 (#3232) 2022-07-07 09:27:01 +02:00
5197a3a461 web: bump moment from 2.29.3 to 2.29.4 in /web (#3226)
Bumps [moment](https://github.com/moment/moment) from 2.29.3 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.3...2.29.4)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-06 22:18:05 +02:00
52be87785f web: Update Web API Client version (#3227)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-07-06 22:07:21 +02:00
8e19fb3a8c release: 2022.7.2 2022-07-06 20:31:48 +02:00
0448dcf655 website/docs: prepare 2022.7.2
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-06 19:17:15 +02:00
b8f74ab9e7 website: bump clsx from 1.2.0 to 1.2.1 in /website (#3222)
Bumps [clsx](https://github.com/lukeed/clsx) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/lukeed/clsx/releases)
- [Commits](https://github.com/lukeed/clsx/compare/v1.2.0...v1.2.1)

---
updated-dependencies:
- dependency-name: clsx
  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>
2022-07-06 09:54:12 +02:00
501ce5cebb core: bump goauthentik.io/api/v3 from 3.2022071.1 to 3.2022071.2 (#3223)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2022071.1 to 3.2022071.2.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2022071.1...v3.2022071.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>
2022-07-06 09:54:02 +02:00
b896ca7ef6 web: fix locale erroring with no pre-hydrated locale setting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-06 09:53:46 +02:00
d497db3010 flows: fix OOB flow incorrectly setting pending user
closes #3224

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-06 09:51:20 +02:00
24f95fdeaa tenants: fix tests for current tenant
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-05 23:47:49 +02:00
d1c4818724 policies: improve api test coverage
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-05 23:20:48 +02:00
9f736a9d99 web: Update Web API Client version (#3220)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-07-05 23:13:56 +02:00
49cce6a968 stages/prompt: add basic file field (#3156)
add basic file field

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-05 23:09:41 +02:00
713337130b web: Update Web API Client version (#3219)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-07-05 23:08:14 +02:00
0a73e7ac9f tenants: add default_locale read only field, pre-hydrate in flows and read in autodetect as first choice
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-05 23:04:25 +02:00
3344af72c2 outposts: cleanup user handling
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-05 22:41:19 +02:00
41eb44137e internal: remove pkg/errors 2022-07-05 20:26:33 +00:00
94a9667d86 web: bump @formatjs/intl-listformat from 7.0.2 to 7.0.3 in /web (#3216) 2022-07-05 09:09:49 +02:00
8b56a7defb core: bump django-silk from 5.0.0 to 5.0.1 (#3217) 2022-07-05 09:09:36 +02:00
5a4b9b4239 core: bump goauthentik.io/api/v3 from 3.2022063.5 to 3.2022071.1 (#3218) 2022-07-05 09:09:24 +02:00
f37308461c web: bump @typescript-eslint/eslint-plugin from 5.30.4 to 5.30.5 in /web (#3213)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.30.4 to 5.30.5.
- [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.30.5/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>
2022-07-04 22:00:37 +02:00
9721098178 web: bump @sentry/browser from 7.4.1 to 7.5.0 in /web (#3215)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 7.4.1 to 7.5.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.4.1...7.5.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>
2022-07-04 22:00:24 +02:00
0ca5e67dad web: bump rapidoc from 9.3.2 to 9.3.3 in /web (#3212)
Bumps [rapidoc](https://github.com/rapi-doc/RapiDoc) from 9.3.2 to 9.3.3.
- [Release notes](https://github.com/rapi-doc/RapiDoc/releases)
- [Commits](https://github.com/rapi-doc/RapiDoc/compare/v9.3.2...v9.3.3)

---
updated-dependencies:
- dependency-name: rapidoc
  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>
2022-07-04 22:00:11 +02:00
da94564d5e web: bump @jackfranklin/rollup-plugin-markdown from 0.3.0 to 0.4.0 in /web (#3209)
web: bump @jackfranklin/rollup-plugin-markdown in /web

Bumps @jackfranklin/rollup-plugin-markdown from 0.3.0 to 0.4.0.

---
updated-dependencies:
- dependency-name: "@jackfranklin/rollup-plugin-markdown"
  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>
2022-07-04 21:59:57 +02:00
1f33237659 web: bump @typescript-eslint/parser from 5.30.4 to 5.30.5 in /web (#3214)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.30.4 to 5.30.5.
- [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.30.5/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>
2022-07-04 21:59:27 +02:00
62e5979c13 web: bump @sentry/tracing from 7.4.1 to 7.5.0 in /web (#3208)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 7.4.1 to 7.5.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.4.1...7.5.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>
2022-07-04 21:59:10 +02:00
8a1e18e087 website: bump rapidoc from 9.3.2 to 9.3.3 in /website (#3210)
Bumps [rapidoc](https://github.com/rapi-doc/RapiDoc) from 9.3.2 to 9.3.3.
- [Release notes](https://github.com/rapi-doc/RapiDoc/releases)
- [Commits](https://github.com/rapi-doc/RapiDoc/compare/v9.3.2...v9.3.3)

---
updated-dependencies:
- dependency-name: rapidoc
  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>
2022-07-04 21:58:19 +02:00
a951daddce web: Update Web API Client version (#3211)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-07-04 21:58:06 +02:00
690f6d444a Merge branch 'version-2022.7' 2022-07-04 21:26:47 +02:00
b733930745 website/docs: add 2022.7 to sidebar
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-04 21:14:49 +02:00
389 changed files with 12225 additions and 9007 deletions

View File

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

View File

@ -0,0 +1,92 @@
name: 'Comment usage instructions on PRs'
description: 'Comment usage instructions on PRs'
inputs:
tag:
description: "Image tag to pull"
required: true
runs:
using: "composite"
steps:
- name: Generate config
id: ev
shell: python
run: |
"""Helper script to get the actual branch name, docker safe"""
import os
from time import time
env_pr_branch = "GITHUB_HEAD_REF"
default_branch = "GITHUB_REF"
sha = "GITHUB_SHA"
branch_name = os.environ[default_branch]
if os.environ.get(env_pr_branch, "") != "":
branch_name = os.environ[env_pr_branch]
should_build = str(os.environ.get("DOCKER_USERNAME", "") != "").lower()
print("##[set-output name=branchName]%s" % branch_name)
print(
"##[set-output name=branchNameContainer]%s"
% branch_name.replace("refs/heads/", "").replace("/", "-")
)
print("##[set-output name=timestamp]%s" % int(time()))
print("##[set-output name=sha]%s" % os.environ[sha])
print("##[set-output name=shouldBuild]%s" % should_build)
import configparser
parser = configparser.ConfigParser()
parser.read(".bumpversion.cfg")
version = parser.get("bumpversion", "current_version")
version_family = ".".join(version.split(".")[:-1])
print("##[set-output name=version]%s" % version)
print("##[set-output name=versionFamily]%s" % version_family)
- name: Find Comment
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: authentik PR Installation instructions
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v2
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
authentik PR Installation instructions
<details>
<summary>Instructions for docker-compose</summary>
Add the following block to your `.env` file:
```shell
AUTHENTIK_IMAGE=ghcr.io/goauthentik/dev-server
AUTHENTIK_TAG=${{ inputs.tag }}
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
```
Afterwards, run the upgrade commands from the latest release notes.
</details>
<details>
<summary>Instructions for Kubernetes</summary>
Add the following block to your `values.yml` file:
```yaml
authentik:
outposts:
container_image_base: ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
image:
repository: ghcr.io/goauthentik/dev-server
tag: ${{ inputs.tag }}
# pullPolicy: Always to ensure you always get the latest version
pullPolicy: Always
```
Afterwards, run the upgrade commands from the latest release notes.
</details>
edit-mode: replace

View File

@ -235,3 +235,9 @@ jobs:
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
platforms: ${{ matrix.arch }}
- name: Comment on PR
if: github.event_name == 'pull_request'
continue-on-error: true
uses: ./.github/actions/comment-pr-instructions
with:
tag: gh-${{ steps.ev.outputs.branchNameContainer }}

View File

@ -111,7 +111,7 @@ jobs:
- uses: actions/setup-go@v3
with:
go-version: "^1.17"
- uses: actions/setup-node@v3.3.0
- uses: actions/setup-node@v3.4.1
with:
node-version: '16'
cache: 'npm'

View File

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.3.0
- uses: actions/setup-node@v3.4.1
with:
node-version: '16'
cache: 'npm'
@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.3.0
- uses: actions/setup-node@v3.4.1
with:
node-version: '16'
cache: 'npm'
@ -47,7 +47,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.3.0
- uses: actions/setup-node@v3.4.1
with:
node-version: '16'
cache: 'npm'
@ -78,7 +78,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.3.0
- uses: actions/setup-node@v3.4.1
with:
node-version: '16'
cache: 'npm'

View File

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.3.0
- uses: actions/setup-node@v3.4.1
with:
node-version: '16'
cache: 'npm'

View File

@ -100,7 +100,7 @@ jobs:
- uses: actions/setup-go@v3
with:
go-version: "^1.17"
- uses: actions/setup-node@v3.3.0
- uses: actions/setup-node@v3.4.1
with:
node-version: '16'
cache: 'npm'

View File

@ -16,15 +16,12 @@ jobs:
echo "PG_PASS=$(openssl rand -base64 32)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env
docker buildx install
docker build \
--no-cache \
-t testing:latest \
-f Dockerfile .
docker build -t testing:latest .
echo "AUTHENTIK_IMAGE=testing" >> .env
echo "AUTHENTIK_TAG=latest" >> .env
docker-compose up --no-start
docker-compose start postgresql redis
docker-compose run -u root server test
docker-compose run -u root server test-all
- name: Extract version number
id: get_version
uses: actions/github-script@v6

View File

@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v3
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v3.3.0
- uses: actions/setup-node@v3.4.1
with:
node-version: '16'
registry-url: 'https://registry.npmjs.org'
@ -35,8 +35,8 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: update-web-api-client
commit-message: "web: Update Web API Client version"
title: "web: Update Web API Client version"
body: "web: Update Web API Client version"
commit-message: "web: bump API Client version"
title: "web: bump API Client version"
body: "web: bump API Client version"
delete-branch: true
signoff: true

14
.vscode/settings.json vendored
View File

@ -20,11 +20,15 @@
"todo-tree.tree.showCountsInTree": true,
"todo-tree.tree.showBadges": true,
"python.formatting.provider": "black",
"files.associations": {
"*.akflow": "json"
},
"yaml.customTags": [
"!Find sequence",
"!KeyOf scalar"
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifierEnding": "js",
"typescript.preferences.importModuleSpecifierEnding": "index",
"typescript.tsdk": "./web/node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
"typescript.enablePromptUseWorkspaceTsdk": true,
"yaml.schemas": {
"./blueprints/schema.json": "blueprints/**/*.yaml"
}
}

View File

@ -2,6 +2,7 @@
FROM --platform=${BUILDPLATFORM} docker.io/node:18 as website-builder
COPY ./website /work/website/
COPY ./blueprints /work/blueprints/
ENV NODE_ENV=production
WORKDIR /work/website
@ -18,7 +19,7 @@ WORKDIR /work/web
RUN npm ci && npm run build
# Stage 3: Poetry to requirements.txt export
FROM docker.io/python:3.10.5-slim-bullseye AS poetry-locker
FROM docker.io/python:3.10.6-slim-bullseye AS poetry-locker
WORKDIR /work
COPY ./pyproject.toml /work
@ -29,7 +30,7 @@ RUN pip install --no-cache-dir poetry && \
poetry export -f requirements.txt --dev --output requirements-dev.txt
# Stage 4: Build go proxy
FROM docker.io/golang:1.18.3-bullseye AS builder
FROM docker.io/golang:1.19.0-bullseye AS go-builder
WORKDIR /work
@ -45,7 +46,7 @@ COPY ./go.sum /work/go.sum
RUN go build -o /work/authentik ./cmd/server/main.go
# Stage 5: Run
FROM docker.io/python:3.10.5-slim-bullseye
FROM docker.io/python:3.10.6-slim-bullseye AS final-image
LABEL org.opencontainers.image.url https://goauthentik.io
LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info.
@ -72,7 +73,7 @@ RUN apt-get update && \
apt-get clean && \
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
mkdir -p /certs /media && \
mkdir -p /certs /media /blueprints && \
mkdir -p /authentik/.ssh && \
chown authentik:authentik /certs /media /authentik/.ssh
@ -81,13 +82,14 @@ COPY ./pyproject.toml /
COPY ./xml /xml
COPY ./tests /tests
COPY ./manage.py /
COPY ./blueprints /blueprints
COPY ./lifecycle/ /lifecycle
COPY --from=builder /work/authentik /authentik-proxy
COPY --from=go-builder /work/authentik /authentik-proxy
COPY --from=web-builder /work/web/dist/ /web/dist/
COPY --from=web-builder /work/web/authentik/ /web/authentik/
COPY --from=website-builder /work/website/help/ /website/help/
USER authentik
USER 1000
ENV TMPDIR /dev/shm/
ENV PYTHONUNBUFFERED 1
@ -95,4 +97,4 @@ ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "/lifecycle/ak", "healthcheck" ]
ENTRYPOINT [ "/lifecycle/ak" ]
ENTRYPOINT [ "/usr/local/bin/dumb-init", "--", "/lifecycle/ak" ]

View File

@ -33,8 +33,8 @@ test:
coverage report
lint-fix:
isort authentik tests lifecycle
black authentik tests lifecycle
isort authentik tests scripts lifecycle
black authentik tests scripts lifecycle
codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w \
authentik \
internal \
@ -52,10 +52,11 @@ lint:
i18n-extract: i18n-extract-core web-extract
i18n-extract-core:
./manage.py makemessages --ignore web --ignore internal --ignore web --ignore web-api --ignore website -l en
ak makemessages --ignore web --ignore internal --ignore web --ignore web-api --ignore website -l en
gen-build:
AUTHENTIK_DEBUG=true ./manage.py spectacular --file schema.yml
AUTHENTIK_DEBUG=true ak make_blueprint_schema > blueprints/schema.json
AUTHENTIK_DEBUG=true ak spectacular --file schema.yml
gen-clean:
rm -rf web/api/src/
@ -91,6 +92,9 @@ gen-client-go:
go mod edit -replace goauthentik.io/api/v3=./gen-go-api
rm -rf config.yaml ./templates/
gen-dev-config:
python -m scripts.generate_config
gen: gen-build gen-clean gen-client-web
migrate:
@ -165,7 +169,16 @@ ci-pyright: ci--meta-debug
pyright e2e lifecycle
ci-pending-migrations: ci--meta-debug
./manage.py makemigrations --check
ak makemigrations --check
install: web-install website-install
poetry install
dev-reset:
dropdb -U postgres -h localhost authentik
createdb -U postgres -h localhost authentik
redis-cli -n 0 flushall
redis-cli -n 1 flushall
redis-cli -n 2 flushall
redis-cli -n 3 flushall
make migrate

View File

@ -6,9 +6,9 @@
| Version | Supported |
| ---------- | ------------------ |
| 2022.5.x | :white_check_mark: |
| 2022.6.x | :white_check_mark: |
| 2022.7.x | :white_check_mark: |
| 2022.8.x | :white_check_mark: |
## Reporting a Vulnerability

View File

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

View File

@ -16,7 +16,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from authentik.core.api.utils import PassiveSerializer
from authentik.outposts.managed import MANAGED_OUTPOST
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost

View File

@ -1,19 +1,20 @@
"""authentik admin app config"""
from importlib import import_module
from django.apps import AppConfig
from prometheus_client import Gauge, Info
from authentik.blueprints.manager import ManagedAppConfig
PROM_INFO = Info("authentik_version", "Currently running authentik version")
GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers")
class AuthentikAdminConfig(AppConfig):
class AuthentikAdminConfig(ManagedAppConfig):
"""authentik admin app config"""
name = "authentik.admin"
label = "authentik_admin"
verbose_name = "authentik Admin"
default = True
def ready(self):
import_module("authentik.admin.signals")
def reconcile_load_admin_signals(self):
"""Load admin signals"""
self.import_module("authentik.admin.signals")

View File

@ -3,6 +3,7 @@ import re
from django.core.cache import cache
from django.core.validators import URLValidator
from django.db import DatabaseError, InternalError, ProgrammingError
from packaging.version import parse
from requests import RequestException
from structlog.stdlib import get_logger
@ -39,7 +40,9 @@ def _set_prom_info():
)
@CELERY_APP.task()
@CELERY_APP.task(
throws=(DatabaseError, ProgrammingError, InternalError),
)
def clear_update_notifications():
"""Clear update notifications on startup if the notification was for the version
we're running now."""

View File

@ -5,10 +5,10 @@ from django.test import TestCase
from django.urls import reverse
from authentik import __version__
from authentik.blueprints.tests import reconcile_app
from authentik.core.models import Group, User
from authentik.core.tasks import clean_expired_models
from authentik.events.monitored_tasks import TaskResultStatus
from authentik.managed.tasks import managed_reconcile
class TestAdminAPI(TestCase):
@ -93,9 +93,8 @@ class TestAdminAPI(TestCase):
response = self.client.get(reverse("authentik_api:apps-list"))
self.assertEqual(response.status_code, 200)
@reconcile_app("authentik_outposts")
def test_system(self):
"""Test system API"""
# pyright: reportGeneralTypeIssues=false
managed_reconcile() # pylint: disable=no-value-for-parameter
response = self.client.get(reverse("authentik_api:admin_system"))
self.assertEqual(response.status_code, 200)

View File

@ -7,7 +7,7 @@ from rest_framework.exceptions import AuthenticationFailed
from rest_framework.request import Request
from structlog.stdlib import get_logger
from authentik.core.middleware import KEY_AUTH_VIA, LOCAL
from authentik.core.middleware import CTX_AUTH_VIA
from authentik.core.models import Token, TokenIntents, User
from authentik.outposts.models import Outpost
from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API
@ -36,14 +36,12 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
auth_credentials = validate_auth(raw_header)
if not auth_credentials:
return None
if not hasattr(LOCAL, "authentik"):
LOCAL.authentik = {}
# first, check traditional tokens
key_token = Token.filter_not_expired(
key=auth_credentials, intent=TokenIntents.INTENT_API
).first()
if key_token:
LOCAL.authentik[KEY_AUTH_VIA] = "api_token"
CTX_AUTH_VIA.set("api_token")
return key_token.user
# then try to auth via JWT
jwt_token = RefreshToken.filter_not_expired(
@ -54,12 +52,12 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
# we want to check the parsed version too
if SCOPE_AUTHENTIK_API not in jwt_token.scope:
raise AuthenticationFailed("Token invalid/expired")
LOCAL.authentik[KEY_AUTH_VIA] = "jwt"
CTX_AUTH_VIA.set("jwt")
return jwt_token.user
# then try to auth via secret key (for embedded outpost/etc)
user = token_secret_key(auth_credentials)
if user:
LOCAL.authentik[KEY_AUTH_VIA] = "secret_key"
CTX_AUTH_VIA.set("secret_key")
return user
raise AuthenticationFailed("Token invalid/expired")
@ -67,7 +65,7 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
def token_secret_key(value: str) -> Optional[User]:
"""Check if the token is the secret key
and return the service account for the managed outpost"""
from authentik.outposts.managed import MANAGED_OUTPOST
from authentik.outposts.apps import MANAGED_OUTPOST
if value != settings.SECRET_KEY:
return None

View File

@ -7,10 +7,10 @@ from guardian.shortcuts import get_anonymous_user
from rest_framework.exceptions import AuthenticationFailed
from authentik.api.authentication import bearer_auth
from authentik.blueprints.tests import reconcile_app
from authentik.core.models import USER_ATTRIBUTE_SA, Token, TokenIntents
from authentik.core.tests.utils import create_test_flow
from authentik.lib.generators import generate_id
from authentik.outposts.managed import OutpostManager
from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API
from authentik.providers.oauth2.models import OAuth2Provider, RefreshToken
@ -42,9 +42,11 @@ class TestAPIAuth(TestCase):
def test_managed_outpost(self):
"""Test managed outpost"""
with self.assertRaises(AuthenticationFailed):
user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
OutpostManager().run()
@reconcile_app("authentik_outposts")
def test_managed_outpost_success(self):
"""Test managed outpost"""
user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
self.assertEqual(user.attributes[USER_ATTRIBUTE_SA], True)

View File

@ -68,10 +68,9 @@ class ConfigView(APIView):
caps.append(Capabilities.CAN_IMPERSONATE)
return caps
@extend_schema(responses={200: ConfigSerializer(many=False)})
def get(self, request: Request) -> Response:
"""Retrieve public configuration options"""
config = ConfigSerializer(
def get_config(self) -> ConfigSerializer:
"""Get Config"""
return ConfigSerializer(
{
"error_reporting": {
"enabled": CONFIG.y("error_reporting.enabled"),
@ -86,4 +85,8 @@ class ConfigView(APIView):
"cache_timeout_reputation": int(CONFIG.y("redis.cache_timeout_reputation")),
}
)
return Response(config.data)
@extend_schema(responses={200: ConfigSerializer(many=False)})
def get(self, request: Request) -> Response:
"""Retrieve public configuration options"""
return Response(self.get_config().data)

View File

@ -12,9 +12,10 @@ from authentik.admin.api.version import VersionView
from authentik.admin.api.workers import WorkerView
from authentik.api.v3.config import ConfigView
from authentik.api.views import APIBrowserView
from authentik.blueprints.api import BlueprintInstanceViewSet
from authentik.core.api.applications import ApplicationViewSet
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
from authentik.core.api.devices import DeviceViewSet
from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
from authentik.core.api.groups import GroupViewSet
from authentik.core.api.propertymappings import PropertyMappingViewSet
from authentik.core.api.providers import ProviderViewSet
@ -131,6 +132,8 @@ router.register("events/notifications", NotificationViewSet)
router.register("events/transports", NotificationTransportViewSet)
router.register("events/rules", NotificationRuleViewSet)
router.register("managed/blueprints", BlueprintInstanceViewSet)
router.register("sources/all", SourceViewSet)
router.register("sources/user_connections/all", UserSourceConnectionViewSet)
router.register("sources/user_connections/oauth", UserOAuthSourceConnectionViewSet)
@ -171,6 +174,11 @@ router.register("authenticators/sms", SMSDeviceViewSet)
router.register("authenticators/static", StaticDeviceViewSet)
router.register("authenticators/totp", TOTPDeviceViewSet)
router.register("authenticators/webauthn", WebAuthnDeviceViewSet)
router.register(
"authenticators/admin/all",
AdminDeviceViewSet,
basename="admin-device",
)
router.register(
"authenticators/admin/duo",
DuoAdminDeviceViewSet,

103
authentik/blueprints/api.py Normal file
View File

@ -0,0 +1,103 @@
"""Serializer mixin for managed models"""
from dataclasses import asdict
from drf_spectacular.utils import extend_schema, inline_serializer
from rest_framework.decorators import action
from rest_framework.fields import CharField, DateTimeField, JSONField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.tasks import BlueprintFile, apply_blueprint, blueprints_find
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.events.utils import sanitize_dict
class ManagedSerializer:
"""Managed Serializer"""
managed = CharField(read_only=True, allow_null=True)
class MetadataSerializer(PassiveSerializer):
"""Serializer for blueprint metadata"""
name = CharField()
labels = JSONField()
class BlueprintInstanceSerializer(ModelSerializer):
"""Info about a single blueprint instance file"""
class Meta:
model = BlueprintInstance
fields = [
"pk",
"name",
"path",
"context",
"last_applied",
"last_applied_hash",
"status",
"enabled",
"managed_models",
"metadata",
]
extra_kwargs = {
"status": {"read_only": True},
"last_applied": {"read_only": True},
"last_applied_hash": {"read_only": True},
"managed_models": {"read_only": True},
"metadata": {"read_only": True},
}
class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
"""Blueprint instances"""
permission_classes = [IsAdminUser]
serializer_class = BlueprintInstanceSerializer
queryset = BlueprintInstance.objects.all()
search_fields = ["name", "path"]
filterset_fields = ["name", "path"]
@extend_schema(
responses={
200: ListSerializer(
child=inline_serializer(
"BlueprintFile",
fields={
"path": CharField(),
"last_m": DateTimeField(),
"hash": CharField(),
"meta": MetadataSerializer(required=False, read_only=True),
},
)
)
}
)
@action(detail=False, pagination_class=None, filter_backends=[])
def available(self, request: Request) -> Response:
"""Get blueprints"""
files: list[BlueprintFile] = blueprints_find.delay().get()
return Response([sanitize_dict(asdict(file)) for file in files])
@permission_required("authentik_blueprints.view_blueprintinstance")
@extend_schema(
request=None,
responses={
200: BlueprintInstanceSerializer(),
},
)
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
def apply(self, request: Request, *args, **kwargs) -> Response:
"""Apply a blueprint"""
blueprint = self.get_object()
apply_blueprint.delay(str(blueprint.pk)).get()
return self.retrieve(request, *args, **kwargs)

View File

@ -0,0 +1,22 @@
"""authentik Blueprints app"""
from authentik.blueprints.manager import ManagedAppConfig
class AuthentikBlueprintsConfig(ManagedAppConfig):
"""authentik Blueprints app"""
name = "authentik.blueprints"
label = "authentik_blueprints"
verbose_name = "authentik Blueprints"
default = True
def reconcile_load_blueprints_v1_tasks(self):
"""Load v1 tasks"""
self.import_module("authentik.blueprints.v1.tasks")
def reconcile_blueprints_discover(self):
"""Run blueprint discovery"""
from authentik.blueprints.v1.tasks import blueprints_discover
blueprints_discover.delay()

View File

@ -0,0 +1,27 @@
"""Apply blueprint from commandline"""
from django.core.management.base import BaseCommand, no_translations
from structlog.stdlib import get_logger
from authentik.blueprints.v1.importer import Importer
LOGGER = get_logger()
class Command(BaseCommand):
"""Apply blueprint from commandline"""
@no_translations
def handle(self, *args, **options):
"""Apply all blueprints in order, abort when one fails to import"""
for blueprint_path in options.get("blueprints", []):
with open(blueprint_path, "r", encoding="utf8") as blueprint_file:
importer = Importer(blueprint_file.read())
valid, logs = importer.validate()
if not valid:
for log in logs:
LOGGER.debug(**log)
raise ValueError("blueprint invalid")
importer.apply()
def add_arguments(self, parser):
parser.add_argument("blueprints", nargs="+", type=str)

View File

@ -0,0 +1,35 @@
"""Generate JSON Schema for blueprints"""
from json import dumps, loads
from pathlib import Path
from django.apps import apps
from django.core.management.base import BaseCommand, no_translations
from structlog.stdlib import get_logger
from authentik.blueprints.v1.importer import is_model_allowed
LOGGER = get_logger()
class Command(BaseCommand):
"""Generate JSON Schema for blueprints"""
schema: dict
@no_translations
def handle(self, *args, **options):
"""Generate JSON Schema for blueprints"""
path = Path(__file__).parent.joinpath("./schema_template.json")
with open(path, "r", encoding="utf-8") as _template_file:
self.schema = loads(_template_file.read())
self.set_model_allowed()
self.stdout.write(dumps(self.schema, indent=4))
def set_model_allowed(self):
"""Set model enum"""
model_names = []
for model in apps.get_models():
if not is_model_allowed(model):
continue
model_names.append(f"{model._meta.app_label}.{model._meta.model_name}")
self.schema["properties"]["entries"]["items"]["properties"]["model"]["enum"] = model_names

View File

@ -0,0 +1,90 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "http://example.com/example.json",
"type": "object",
"title": "authentik Blueprint schema",
"default": {},
"required": [
"version",
"entries"
],
"properties": {
"version": {
"$id": "#/properties/version",
"type": "integer",
"title": "Blueprint version",
"default": 1
},
"metadata": {
"$id": "#/properties/metadata",
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"labels": {
"type": "object"
}
}
},
"context": {
"$id": "#/properties/context",
"type": "object",
"additionalProperties": true
},
"entries": {
"type": "array",
"items": {
"$id": "#entry",
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"type": "string",
"enum": [
"placeholder"
]
},
"id": {
"type": "string"
},
"attrs": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Commonly available field, may not exist on all models"
}
},
"default": {},
"additionalProperties": true
},
"identifiers": {
"type": "object",
"properties": {
"pk": {
"description": "Commonly available field, may not exist on all models",
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"format": "uuid"
}
]
}
},
"additionalProperties": true
}
}
}
}
}
}

View File

@ -0,0 +1,44 @@
"""Managed objects manager"""
from importlib import import_module
from inspect import ismethod
from django.apps import AppConfig
from django.db import DatabaseError, InternalError, ProgrammingError
from structlog.stdlib import BoundLogger, get_logger
LOGGER = get_logger()
class ManagedAppConfig(AppConfig):
"""Basic reconciliation logic for apps"""
_logger: BoundLogger
def __init__(self, app_name: str, *args, **kwargs) -> None:
super().__init__(app_name, *args, **kwargs)
self._logger = get_logger().bind(app_name=app_name)
def ready(self) -> None:
self.reconcile()
return super().ready()
def import_module(self, path: str):
"""Load module"""
import_module(path)
def reconcile(self) -> None:
"""reconcile ourselves"""
prefix = "reconcile_"
for meth_name in dir(self):
meth = getattr(self, meth_name)
if not ismethod(meth):
continue
if not meth_name.startswith(prefix):
continue
name = meth_name.replace(prefix, "")
try:
self._logger.debug("Starting reconciler", name=name)
meth()
self._logger.debug("Successfully reconciled", name=name)
except (DatabaseError, ProgrammingError, InternalError) as exc:
self._logger.debug("Failed to run reconcile", name=name, exc=exc)

View File

@ -0,0 +1,134 @@
# Generated by Django 4.0.6 on 2022-07-31 17:35
import uuid
from glob import glob
from pathlib import Path
import django.contrib.postgres.fields
from dacite import from_dict
from django.apps.registry import Apps
from django.conf import settings
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from yaml import load
from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_SYSTEM
from authentik.lib.config import CONFIG
def check_blueprint_v1_file(BlueprintInstance: type["BlueprintInstance"], path: Path):
"""Check if blueprint should be imported"""
from authentik.blueprints.models import BlueprintInstanceStatus
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata
from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_INSTANTIATE
with open(path, "r", encoding="utf-8") as blueprint_file:
raw_blueprint = load(blueprint_file.read(), BlueprintLoader)
if not raw_blueprint:
return
metadata = raw_blueprint.get("metadata", None)
version = raw_blueprint.get("version", 1)
if version != 1:
return
blueprint_file.seek(0)
instance: BlueprintInstance = BlueprintInstance.objects.filter(path=path).first()
rel_path = path.relative_to(Path(CONFIG.y("blueprints_dir")))
meta = None
if metadata:
meta = from_dict(BlueprintMetadata, metadata)
if meta.labels.get(LABEL_AUTHENTIK_INSTANTIATE, "").lower() == "false":
return
if not instance:
instance = BlueprintInstance(
name=meta.name if meta else str(rel_path),
path=str(rel_path),
context={},
status=BlueprintInstanceStatus.UNKNOWN,
enabled=True,
managed_models=[],
last_applied_hash="",
metadata=metadata,
)
instance.save()
def migration_blueprint_import(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
BlueprintInstance = apps.get_model("authentik_blueprints", "BlueprintInstance")
Flow = apps.get_model("authentik_flows", "Flow")
db_alias = schema_editor.connection.alias
for file in glob(f"{CONFIG.y('blueprints_dir')}/**/*.yaml", recursive=True):
check_blueprint_v1_file(BlueprintInstance, Path(file))
for blueprint in BlueprintInstance.objects.using(db_alias).all():
# If we already have flows (and we should always run before flow migrations)
# then this is an existing install and we want to disable all blueprints
if Flow.objects.using(db_alias).all().exists():
blueprint.enabled = False
# System blueprints are always enabled
if blueprint.metadata.get("labels", {}).get(LABEL_AUTHENTIK_SYSTEM, "").lower() == "true":
blueprint.enabled = True
blueprint.save()
class Migration(migrations.Migration):
initial = True
dependencies = [("authentik_flows", "0001_initial")]
operations = [
migrations.CreateModel(
name="BlueprintInstance",
fields=[
("created", models.DateTimeField(auto_now_add=True)),
("last_updated", models.DateTimeField(auto_now=True)),
(
"managed",
models.TextField(
default=None,
help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
null=True,
unique=True,
verbose_name="Managed by authentik",
),
),
(
"instance_uuid",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
("name", models.TextField()),
("metadata", models.JSONField(default=dict)),
("path", models.TextField()),
("context", models.JSONField(default=dict)),
("last_applied", models.DateTimeField(auto_now=True)),
("last_applied_hash", models.TextField()),
(
"status",
models.TextField(
choices=[
("successful", "Successful"),
("warning", "Warning"),
("error", "Error"),
("orphaned", "Orphaned"),
("unknown", "Unknown"),
]
),
),
("enabled", models.BooleanField(default=True)),
(
"managed_models",
django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), default=list, size=None
),
),
],
options={
"verbose_name": "Blueprint Instance",
"verbose_name_plural": "Blueprint Instances",
"unique_together": {("name", "path")},
},
),
migrations.RunPython(migration_blueprint_import),
]

View File

@ -0,0 +1,79 @@
"""Managed Object models"""
from uuid import uuid4
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
class ManagedModel(models.Model):
"""Model which can be managed by authentik exclusively"""
managed = models.TextField(
default=None,
null=True,
verbose_name=_("Managed by authentik"),
help_text=_(
(
"Objects which are managed by authentik. These objects are created and updated "
"automatically. This is flag only indicates that an object can be overwritten by "
"migrations. You can still modify the objects via the API, but expect changes "
"to be overwritten in a later update."
)
),
unique=True,
)
class Meta:
abstract = True
class BlueprintInstanceStatus(models.TextChoices):
"""Instance status"""
SUCCESSFUL = "successful"
WARNING = "warning"
ERROR = "error"
ORPHANED = "orphaned"
UNKNOWN = "unknown"
class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
"""Instance of a single blueprint. Can be parameterized via context attribute when
blueprint in `path` has inputs."""
instance_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
name = models.TextField()
metadata = models.JSONField(default=dict)
path = models.TextField()
context = models.JSONField(default=dict)
last_applied = models.DateTimeField(auto_now=True)
last_applied_hash = models.TextField()
status = models.TextField(choices=BlueprintInstanceStatus.choices)
enabled = models.BooleanField(default=True)
managed_models = ArrayField(models.TextField(), default=list)
@property
def serializer(self) -> Serializer:
from authentik.blueprints.api import BlueprintInstanceSerializer
return BlueprintInstanceSerializer
def __str__(self) -> str:
return f"Blueprint Instance {self.name}"
class Meta:
verbose_name = _("Blueprint Instance")
verbose_name_plural = _("Blueprint Instances")
unique_together = (
(
"name",
"path",
),
)

View File

@ -0,0 +1,12 @@
"""blueprint Settings"""
from celery.schedules import crontab
from authentik.lib.utils.time import fqdn_rand
CELERY_BEAT_SCHEDULE = {
"blueprints_v1_discover": {
"task": "authentik.blueprints.v1.tasks.blueprints_discover",
"schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"),
"options": {"queue": "authentik_scheduled"},
},
}

View File

@ -0,0 +1,49 @@
"""Blueprint helpers"""
from functools import wraps
from pathlib import Path
from typing import Callable
from django.apps import apps
from authentik.blueprints.manager import ManagedAppConfig
from authentik.lib.config import CONFIG
def apply_blueprint(*files: str):
"""Apply blueprint before test"""
from authentik.blueprints.v1.importer import Importer
def wrapper_outer(func: Callable):
"""Apply blueprint before test"""
@wraps(func)
def wrapper(*args, **kwargs):
base_path = Path(CONFIG.y("blueprints_dir"))
for file in files:
full_path = Path(base_path, file)
with full_path.open("r", encoding="utf-8") as _file:
Importer(_file.read()).apply()
return func(*args, **kwargs)
return wrapper
return wrapper_outer
def reconcile_app(app_name: str):
"""Re-reconcile AppConfig methods"""
def wrapper_outer(func: Callable):
"""Re-reconcile AppConfig methods"""
@wraps(func)
def wrapper(*args, **kwargs):
config = apps.get_app_config(app_name)
if isinstance(config, ManagedAppConfig):
config.reconcile()
return func(*args, **kwargs)
return wrapper
return wrapper_outer

View File

@ -0,0 +1,37 @@
"""test packaged blueprints"""
from glob import glob
from pathlib import Path
from typing import Callable
from django.test import TransactionTestCase
from django.utils.text import slugify
from authentik.blueprints.tests import apply_blueprint
from authentik.blueprints.v1.importer import Importer
from authentik.tenants.models import Tenant
class TestBundled(TransactionTestCase):
"""Empty class, test methods are added dynamically"""
@apply_blueprint("default/90-default-tenant.yaml")
def test_decorator_static(self):
"""Test @apply_blueprint decorator"""
self.assertTrue(Tenant.objects.filter(domain="authentik-default").exists())
def blueprint_tester(file_name: str) -> Callable:
"""This is used instead of subTest for better visibility"""
def tester(self: TestBundled):
with open(file_name, "r", encoding="utf8") as flow_yaml:
importer = Importer(flow_yaml.read())
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
return tester
for flow_file in glob("blueprints/**/*.yaml", recursive=True):
method_name = slugify(Path(flow_file).stem).replace("-", "_").replace(".", "_")
setattr(TestBundled, f"test_flow_{method_name}", blueprint_tester(flow_file))

View File

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

View File

@ -1,53 +1,56 @@
"""Test flow transfer"""
from json import dumps
"""Test blueprints v1"""
from django.test import TransactionTestCase
from authentik.blueprints.v1.exporter import Exporter
from authentik.blueprints.v1.importer import Importer, transaction_rollback
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.transfer.common import DataclassEncoder
from authentik.flows.transfer.exporter import FlowExporter
from authentik.flows.transfer.importer import FlowImporter, transaction_rollback
from authentik.lib.generators import generate_id
from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import PolicyBinding
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
from authentik.stages.user_login.models import UserLoginStage
STATIC_PROMPT_EXPORT = """{
"version": 1,
"entries": [
{
"identifiers": {
"pk": "cb954fd4-65a5-4ad9-b1ee-180ee9559cf4"
},
"model": "authentik_stages_prompt.prompt",
"attrs": {
"field_key": "username",
"label": "Username",
"type": "username",
"required": true,
"placeholder": "Username",
"order": 0
}
}
]
}"""
STATIC_PROMPT_EXPORT = """version: 1
entries:
- identifiers:
pk: cb954fd4-65a5-4ad9-b1ee-180ee9559cf4
model: authentik_stages_prompt.prompt
attrs:
field_key: username
label: Username
type: username
required: true
placeholder: Username
order: 0
"""
YAML_TAG_TESTS = """version: 1
context:
foo: bar
entries:
- attrs:
expression: return True
identifiers:
name: !Format [foo-%s-%s, !Context foo, !Context bar]
id: default-source-enrollment-if-username
model: authentik_policies_expression.expressionpolicy
"""
class TestFlowTransfer(TransactionTestCase):
"""Test flow transfer"""
class TestBlueprintsV1(TransactionTestCase):
"""Test Blueprints"""
def test_bundle_invalid_format(self):
"""Test bundle with invalid format"""
importer = FlowImporter('{"version": 3}')
self.assertFalse(importer.validate())
importer = FlowImporter(
def test_blueprint_invalid_format(self):
"""Test blueprint with invalid format"""
importer = Importer('{"version": 3}')
self.assertFalse(importer.validate()[0])
importer = Importer(
(
'{"version": 1,"entries":[{"identifiers":{},"attrs":{},'
'"model": "authentik_core.User"}]}'
)
)
self.assertFalse(importer.validate())
self.assertFalse(importer.validate()[0])
def test_export_validate_import(self):
"""Test export and validate it"""
@ -67,13 +70,13 @@ class TestFlowTransfer(TransactionTestCase):
order=0,
)
exporter = FlowExporter(flow)
exporter = Exporter(flow)
export = exporter.export()
self.assertEqual(len(export.entries), 3)
export_json = exporter.export_to_string()
export_yaml = exporter.export_to_string()
importer = FlowImporter(export_json)
self.assertTrue(importer.validate())
importer = Importer(export_yaml)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
self.assertTrue(Flow.objects.filter(slug=flow_slug).exists())
@ -82,18 +85,26 @@ class TestFlowTransfer(TransactionTestCase):
"""Test export and import it twice"""
count_initial = Prompt.objects.filter(field_key="username").count()
importer = FlowImporter(STATIC_PROMPT_EXPORT)
self.assertTrue(importer.validate())
importer = Importer(STATIC_PROMPT_EXPORT)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
count_before = Prompt.objects.filter(field_key="username").count()
self.assertEqual(count_initial + 1, count_before)
importer = FlowImporter(STATIC_PROMPT_EXPORT)
importer = Importer(STATIC_PROMPT_EXPORT)
self.assertTrue(importer.apply())
self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before)
def test_import_yaml_tags(self):
"""Test some yaml tags"""
ExpressionPolicy.objects.filter(name="foo-foo-bar").delete()
importer = Importer(YAML_TAG_TESTS, {"bar": "baz"})
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
self.assertTrue(ExpressionPolicy.objects.filter(name="foo-foo-bar"))
def test_export_validate_import_policies(self):
"""Test export and validate it"""
flow_slug = generate_id()
@ -115,13 +126,11 @@ class TestFlowTransfer(TransactionTestCase):
fsb = FlowStageBinding.objects.create(target=flow, stage=user_login, order=0)
PolicyBinding.objects.create(policy=flow_policy, target=fsb, order=0)
exporter = FlowExporter(flow)
export = exporter.export()
exporter = Exporter(flow)
export_yaml = exporter.export_to_string()
export_json = dumps(export, cls=DataclassEncoder)
importer = FlowImporter(export_json)
self.assertTrue(importer.validate())
importer = Importer(export_yaml)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
self.assertTrue(UserLoginStage.objects.filter(name=stage_name).exists())
self.assertTrue(Flow.objects.filter(slug=flow_slug).exists())
@ -160,11 +169,10 @@ class TestFlowTransfer(TransactionTestCase):
FlowStageBinding.objects.create(target=flow, stage=first_stage, order=0)
exporter = FlowExporter(flow)
export = exporter.export()
export_json = dumps(export, cls=DataclassEncoder)
exporter = Exporter(flow)
export_yaml = exporter.export_to_string()
importer = FlowImporter(export_json)
importer = Importer(export_yaml)
self.assertTrue(importer.validate())
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())

View File

@ -0,0 +1,45 @@
"""Test blueprints v1 api"""
from json import loads
from tempfile import NamedTemporaryFile, mkdtemp
from django.urls import reverse
from rest_framework.test import APITestCase
from yaml import dump
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.config import CONFIG
TMP = mkdtemp("authentik-blueprints")
class TestBlueprintsV1API(APITestCase):
"""Test Blueprints API"""
def setUp(self) -> None:
self.user = create_test_admin_user()
self.client.force_login(self.user)
@CONFIG.patch("blueprints_dir", TMP)
def test_api_available(self):
"""Test valid file"""
with NamedTemporaryFile(mode="w+", suffix=".yaml", dir=TMP) as file:
file.write(
dump(
{
"version": 1,
"entries": [],
}
)
)
file.flush()
res = self.client.get(reverse("authentik_api:blueprintinstance-available"))
self.assertEqual(res.status_code, 200)
response = loads(res.content.decode())
self.assertEqual(len(response), 1)
self.assertEqual(
response[0]["hash"],
(
"e52bb445b03cd36057258dc9f0ce0fbed8278498ee1470e45315293e5f026d1bd1f9b352"
"6871c0003f5c07be5c3316d9d4a08444bd8fed1b3f03294e51e44522"
),
)

View File

@ -0,0 +1,140 @@
"""Test blueprints v1 tasks"""
from tempfile import NamedTemporaryFile, mkdtemp
from django.test import TransactionTestCase
from yaml import dump
from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discover, blueprints_find
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
TMP = mkdtemp("authentik-blueprints")
class TestBlueprintsV1Tasks(TransactionTestCase):
"""Test Blueprints v1 Tasks"""
@CONFIG.patch("blueprints_dir", TMP)
def test_invalid_file_syntax(self):
"""Test syntactically invalid file"""
with NamedTemporaryFile(suffix=".yaml", dir=TMP) as file:
file.write(b"{")
file.flush()
blueprints = blueprints_find()
self.assertEqual(blueprints, [])
@CONFIG.patch("blueprints_dir", TMP)
def test_invalid_file_version(self):
"""Test invalid file"""
with NamedTemporaryFile(suffix=".yaml", dir=TMP) as file:
file.write(b"version: 2")
file.flush()
blueprints = blueprints_find()
self.assertEqual(blueprints, [])
@CONFIG.patch("blueprints_dir", TMP)
def test_valid(self):
"""Test valid file"""
with NamedTemporaryFile(mode="w+", suffix=".yaml", dir=TMP) as file:
file.write(
dump(
{
"version": 1,
"entries": [],
}
)
)
file.flush()
blueprints_discover() # pylint: disable=no-value-for-parameter
self.assertEqual(
BlueprintInstance.objects.first().last_applied_hash,
(
"e52bb445b03cd36057258dc9f0ce0fbed8278498ee1470e45315293e5f026d1b"
"d1f9b3526871c0003f5c07be5c3316d9d4a08444bd8fed1b3f03294e51e44522"
),
)
self.assertEqual(BlueprintInstance.objects.first().metadata, {})
@CONFIG.patch("blueprints_dir", TMP)
def test_valid_updated(self):
"""Test valid file"""
with NamedTemporaryFile(mode="w+", suffix=".yaml", dir=TMP) as file:
file.write(
dump(
{
"version": 1,
"entries": [],
}
)
)
file.flush()
blueprints_discover() # pylint: disable=no-value-for-parameter
self.assertEqual(
BlueprintInstance.objects.first().last_applied_hash,
(
"e52bb445b03cd36057258dc9f0ce0fbed8278498ee1470e45315293e5f026d1b"
"d1f9b3526871c0003f5c07be5c3316d9d4a08444bd8fed1b3f03294e51e44522"
),
)
self.assertEqual(BlueprintInstance.objects.first().metadata, {})
file.write(
dump(
{
"version": 1,
"entries": [],
"metadata": {
"name": "foo",
},
}
)
)
file.flush()
blueprints_discover() # pylint: disable=no-value-for-parameter
self.assertEqual(
BlueprintInstance.objects.first().last_applied_hash,
(
"fc62fea96067da8592bdf90927246d0ca150b045447df93b0652a0e20a8bc327"
"681510b5db37ea98759c61f9a98dd2381f46a3b5a2da69dfb45158897f14e824"
),
)
self.assertEqual(
BlueprintInstance.objects.first().metadata,
{
"name": "foo",
"labels": {},
},
)
@CONFIG.patch("blueprints_dir", TMP)
def test_valid_disabled(self):
"""Test valid file"""
with NamedTemporaryFile(mode="w+", suffix=".yaml", dir=TMP) as file:
file.write(
dump(
{
"version": 1,
"entries": [],
}
)
)
file.flush()
instance: BlueprintInstance = BlueprintInstance.objects.create(
name=generate_id(),
path=file.name,
enabled=False,
status=BlueprintInstanceStatus.UNKNOWN,
)
instance.refresh_from_db()
self.assertEqual(instance.last_applied_hash, "")
self.assertEqual(
instance.status,
BlueprintInstanceStatus.UNKNOWN,
)
apply_blueprint(instance.pk) # pylint: disable=no-value-for-parameter
instance.refresh_from_db()
self.assertEqual(instance.last_applied_hash, "")
self.assertEqual(
instance.status,
BlueprintInstanceStatus.UNKNOWN,
)

View File

View File

@ -0,0 +1,244 @@
"""transfer common classes"""
from collections import OrderedDict
from dataclasses import asdict, dataclass, field, is_dataclass
from enum import Enum
from typing import Any, Optional
from uuid import UUID
from django.apps import apps
from django.db.models import Model, Q
from rest_framework.fields import Field
from rest_framework.serializers import Serializer
from yaml import SafeDumper, SafeLoader, ScalarNode, SequenceNode
from authentik.lib.models import SerializerModel
from authentik.lib.sentry import SentryIgnoredException
from authentik.policies.models import PolicyBindingModel
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
"""Get object's attributes via their serializer, and convert it to a normal dict"""
serializer: Serializer = obj.serializer(obj)
data = dict(serializer.data)
for field_name, _field in serializer.fields.items():
_field: Field
if field_name not in data:
continue
if _field.read_only:
data.pop(field_name, None)
if _field.default == data.get(field_name, None):
data.pop(field_name, None)
if field_name.endswith("_set"):
data.pop(field_name, None)
return data
@dataclass
class BlueprintEntry:
"""Single entry of a bundle"""
identifiers: dict[str, Any]
model: str
attrs: Optional[dict[str, Any]] = field(default_factory=dict)
# pylint: disable=invalid-name
id: Optional[str] = None
_instance: Optional[Model] = None
@staticmethod
def from_model(model: SerializerModel, *extra_identifier_names: str) -> "BlueprintEntry":
"""Convert a SerializerModel instance to a Bundle Entry"""
identifiers = {
"pk": model.pk,
}
all_attrs = get_attrs(model)
for extra_identifier_name in extra_identifier_names:
identifiers[extra_identifier_name] = all_attrs.pop(extra_identifier_name)
return BlueprintEntry(
identifiers=identifiers,
model=f"{model._meta.app_label}.{model._meta.model_name}",
attrs=all_attrs,
)
def tag_resolver(self, value: Any, blueprint: "Blueprint") -> Any:
"""Check if we have any special tags that need handling"""
if isinstance(value, YAMLTag):
return value.resolve(self, blueprint)
if isinstance(value, dict):
for key, inner_value in value.items():
value[key] = self.tag_resolver(inner_value, blueprint)
if isinstance(value, list):
for idx, inner_value in enumerate(value):
value[idx] = self.tag_resolver(inner_value, blueprint)
return value
def get_attrs(self, blueprint: "Blueprint") -> dict[str, Any]:
"""Get attributes of this entry, with all yaml tags resolved"""
return self.tag_resolver(self.attrs, blueprint)
def get_identifiers(self, blueprint: "Blueprint") -> dict[str, Any]:
"""Get attributes of this entry, with all yaml tags resolved"""
return self.tag_resolver(self.identifiers, blueprint)
@dataclass
class BlueprintMetadata:
"""Optional blueprint metadata"""
name: str
labels: dict[str, str] = field(default_factory=dict)
@dataclass
class Blueprint:
"""Dataclass used for a full export"""
version: int = field(default=1)
entries: list[BlueprintEntry] = field(default_factory=list)
metadata: Optional[BlueprintMetadata] = field(default=None)
context: Optional[dict] = field(default_factory=dict)
class YAMLTag:
"""Base class for all YAML Tags"""
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
"""Implement yaml tag logic"""
raise NotImplementedError
class KeyOf(YAMLTag):
"""Reference another object by their ID"""
id_from: str
# pylint: disable=unused-argument
def __init__(self, loader: "BlueprintLoader", node: ScalarNode) -> None:
super().__init__()
self.id_from = node.value
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
for _entry in blueprint.entries:
if _entry.id == self.id_from and _entry._instance:
# Special handling for PolicyBindingModels, as they'll have a different PK
# which is used when creating policy bindings
if (
isinstance(_entry._instance, PolicyBindingModel)
and entry.model.lower() == "authentik_policies.policybinding"
):
return _entry._instance.pbm_uuid
return _entry._instance.pk
raise ValueError(
f"KeyOf: failed to find entry with `id` of `{self.id_from}` and a model instance"
)
class Context(YAMLTag):
"""Lookup key from instance context"""
key: str
default: Optional[Any]
# pylint: disable=unused-argument
def __init__(self, loader: "BlueprintLoader", node: ScalarNode | SequenceNode) -> None:
super().__init__()
self.default = None
if isinstance(node, ScalarNode):
self.key = node.value
if isinstance(node, SequenceNode):
self.key = node.value[0].value
self.default = node.value[1].value
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
value = self.default
if self.key in blueprint.context:
value = blueprint.context[self.key]
return value
class Format(YAMLTag):
"""Format a string"""
format_string: str
args: list[Any]
# pylint: disable=unused-argument
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
super().__init__()
self.format_string = node.value[0].value
self.args = []
for raw_node in node.value[1:]:
self.args.append(raw_node.value)
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
try:
print(self.format_string)
print(self.args)
return self.format_string % tuple(self.args)
except TypeError as exc:
raise EntryInvalidError(exc)
class Find(YAMLTag):
"""Find any object"""
model_name: str
conditions: list[list]
model_class: type[Model]
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
super().__init__()
self.model_name = node.value[0].value
self.model_class = apps.get_model(*self.model_name.split("."))
self.conditions = []
for raw_node in node.value[1:]:
values = []
for node_values in raw_node.value:
values.append(loader.construct_object(node_values))
self.conditions.append(values)
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
query = Q()
for cond in self.conditions:
query &= Q(**{cond[0]: cond[1]})
instance = self.model_class.objects.filter(query).first()
if instance:
return instance.pk
return None
class BlueprintDumper(SafeDumper):
"""Dump dataclasses to yaml"""
default_flow_style = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_representer(UUID, lambda self, data: self.represent_str(str(data)))
self.add_representer(OrderedDict, lambda self, data: self.represent_dict(dict(data)))
self.add_representer(Enum, lambda self, data: self.represent_str(data.value))
def represent(self, data) -> None:
if is_dataclass(data):
data = asdict(data)
return super().represent(data)
class BlueprintLoader(SafeLoader):
"""Loader for blueprints with custom tag support"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_constructor("!KeyOf", KeyOf)
self.add_constructor("!Find", Find)
self.add_constructor("!Context", Context)
self.add_constructor("!Format", Format)
class EntryInvalidError(SentryIgnoredException):
"""Error raised when an entry is invalid"""

View File

@ -1,18 +1,18 @@
"""Flow exporter"""
from json import dumps
from typing import Iterator
from uuid import UUID
from django.db.models import Q
from yaml import dump
from authentik.blueprints.v1.common import Blueprint, BlueprintDumper, BlueprintEntry
from authentik.flows.models import Flow, FlowStageBinding, Stage
from authentik.flows.transfer.common import DataclassEncoder, FlowBundle, FlowBundleEntry
from authentik.policies.models import Policy, PolicyBinding
from authentik.stages.prompt.models import PromptStage
class FlowExporter:
"""Export flow with attached stages into json"""
class Exporter:
"""Export flow with attached stages into yaml"""
flow: Flow
with_policies: bool
@ -31,21 +31,21 @@ class FlowExporter:
"pbm_uuid", flat=True
)
def walk_stages(self) -> Iterator[FlowBundleEntry]:
"""Convert all stages attached to self.flow into FlowBundleEntry objects"""
def walk_stages(self) -> Iterator[BlueprintEntry]:
"""Convert all stages attached to self.flow into BlueprintEntry objects"""
stages = Stage.objects.filter(flow=self.flow).select_related().select_subclasses()
for stage in stages:
if isinstance(stage, PromptStage):
pass
yield FlowBundleEntry.from_model(stage, "name")
yield BlueprintEntry.from_model(stage, "name")
def walk_stage_bindings(self) -> Iterator[FlowBundleEntry]:
"""Convert all bindings attached to self.flow into FlowBundleEntry objects"""
def walk_stage_bindings(self) -> Iterator[BlueprintEntry]:
"""Convert all bindings attached to self.flow into BlueprintEntry objects"""
bindings = FlowStageBinding.objects.filter(target=self.flow).select_related()
for binding in bindings:
yield FlowBundleEntry.from_model(binding, "target", "stage", "order")
yield BlueprintEntry.from_model(binding, "target", "stage", "order")
def walk_policies(self) -> Iterator[FlowBundleEntry]:
def walk_policies(self) -> Iterator[BlueprintEntry]:
"""Walk over all policies. This is done at the beginning of the export for stages that have
a direct foreign key to a policy."""
# Special case for PromptStage as that has a direct M2M to policy, we have to ensure
@ -54,28 +54,28 @@ class FlowExporter:
query = Q(bindings__in=self.pbm_uuids) | Q(promptstage__in=prompt_stages)
policies = Policy.objects.filter(query).select_related()
for policy in policies:
yield FlowBundleEntry.from_model(policy)
yield BlueprintEntry.from_model(policy)
def walk_policy_bindings(self) -> Iterator[FlowBundleEntry]:
def walk_policy_bindings(self) -> Iterator[BlueprintEntry]:
"""Walk over all policybindings relative to us. This is run at the end of the export, as
we are sure all objects exist now."""
bindings = PolicyBinding.objects.filter(target__in=self.pbm_uuids).select_related()
for binding in bindings:
yield FlowBundleEntry.from_model(binding, "policy", "target", "order")
yield BlueprintEntry.from_model(binding, "policy", "target", "order")
def walk_stage_prompts(self) -> Iterator[FlowBundleEntry]:
def walk_stage_prompts(self) -> Iterator[BlueprintEntry]:
"""Walk over all prompts associated with any PromptStages"""
prompt_stages = PromptStage.objects.filter(flow=self.flow)
for stage in prompt_stages:
for prompt in stage.fields.all():
yield FlowBundleEntry.from_model(prompt)
yield BlueprintEntry.from_model(prompt)
def export(self) -> FlowBundle:
def export(self) -> Blueprint:
"""Create a list of all objects including the flow"""
if self.with_policies:
self._prepare_pbm()
bundle = FlowBundle()
bundle.entries.append(FlowBundleEntry.from_model(self.flow, "slug"))
bundle = Blueprint()
bundle.entries.append(BlueprintEntry.from_model(self.flow, "slug"))
if self.with_stage_prompts:
bundle.entries.extend(self.walk_stage_prompts())
if self.with_policies:
@ -87,6 +87,6 @@ class FlowExporter:
return bundle
def export_to_string(self) -> str:
"""Call export and convert it to json"""
"""Call export and convert it to yaml"""
bundle = self.export()
return dumps(bundle, cls=DataclassEncoder)
return dump(bundle, Dumper=BlueprintDumper)

View File

@ -1,11 +1,11 @@
"""Flow importer"""
"""Blueprint importer"""
from contextlib import contextmanager
from copy import deepcopy
from json import loads
from typing import Any
from typing import Any, Optional
from dacite import from_dict
from dacite.exceptions import DaciteError
from deepmerge import always_merger
from django.apps import apps
from django.db import transaction
from django.db.models import Model
@ -14,14 +14,51 @@ from django.db.utils import IntegrityError
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import BaseSerializer, Serializer
from structlog.stdlib import BoundLogger, get_logger
from structlog.testing import capture_logs
from structlog.types import EventDict
from yaml import load
from authentik.flows.models import Flow, FlowStageBinding, Stage
from authentik.flows.transfer.common import EntryInvalidError, FlowBundle, FlowBundleEntry
from authentik.blueprints.v1.common import (
Blueprint,
BlueprintEntry,
BlueprintLoader,
EntryInvalidError,
)
from authentik.core.models import (
AuthenticatedSession,
PropertyMapping,
Provider,
Source,
UserSourceConnection,
)
from authentik.flows.models import Stage
from authentik.lib.models import SerializerModel
from authentik.policies.models import Policy, PolicyBinding
from authentik.stages.prompt.models import Prompt
from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel
ALLOWED_MODELS = (Flow, FlowStageBinding, Stage, Policy, PolicyBinding, Prompt)
def is_model_allowed(model: type[Model]) -> bool:
"""Check if model is allowed"""
# pylint: disable=imported-auth-user
from django.contrib.auth.models import Group as DjangoGroup
from django.contrib.auth.models import User as DjangoUser
excluded_models = (
DjangoUser,
DjangoGroup,
# Base classes
Provider,
Source,
PropertyMapping,
UserSourceConnection,
Stage,
OutpostServiceConnection,
Policy,
PolicyBindingModel,
# Classes that have other dependencies
AuthenticatedSession,
)
return model not in excluded_models
@contextmanager
@ -34,19 +71,29 @@ def transaction_rollback():
atomic.__exit__(IntegrityError, None, None)
class FlowImporter:
"""Import Flow from json"""
class Importer:
"""Import Blueprint from YAML"""
logger: BoundLogger
def __init__(self, json_input: str):
def __init__(self, yaml_input: str, context: Optional[dict] = None):
self.__pk_map: dict[Any, Model] = {}
self.logger = get_logger()
import_dict = loads(json_input)
import_dict = load(yaml_input, BlueprintLoader)
try:
self.__import = from_dict(FlowBundle, import_dict)
self.__import = from_dict(Blueprint, import_dict)
except DaciteError as exc:
raise EntryInvalidError from exc
context = {}
always_merger.merge(context, self.__import.context)
if context:
always_merger.merge(context, context)
self.__import.context = context
@property
def blueprint(self) -> Blueprint:
"""Get imported blueprint"""
return self.__import
def __update_pks_for_attrs(self, attrs: dict[str, Any]) -> dict[str, Any]:
"""Replace any value if it is a known primary key of an other object"""
@ -75,7 +122,9 @@ class FlowImporter:
"""Generate an or'd query from all identifiers in an entry"""
# Since identifiers can also be pk-references to other objects (see FlowStageBinding)
# we have to ensure those references are also replaced
main_query = Q(pk=attrs["pk"])
main_query = Q()
if "pk" in attrs:
main_query = Q(pk=attrs["pk"])
sub_query = Q()
for identifier, value in attrs.items():
if isinstance(value, dict):
@ -85,19 +134,22 @@ class FlowImporter:
sub_query &= Q(**{identifier: value})
return main_query | sub_query
def _validate_single(self, entry: FlowBundleEntry) -> BaseSerializer:
def _validate_single(self, entry: BlueprintEntry) -> BaseSerializer:
"""Validate a single entry"""
model_app_label, model_name = entry.model.split(".")
model: type[SerializerModel] = apps.get_model(model_app_label, model_name)
if not isinstance(model(), ALLOWED_MODELS):
# Don't use isinstance since we don't want to check for inheritance
if not is_model_allowed(model):
raise EntryInvalidError(f"Model {model} not allowed")
if entry.identifiers == {}:
raise EntryInvalidError("No identifiers")
# If we try to validate without referencing a possible instance
# we'll get a duplicate error, hence we load the model here and return
# the full serializer for later usage
# Because a model might have multiple unique columns, we chain all identifiers together
# to create an OR query.
updated_identifiers = self.__update_pks_for_attrs(entry.identifiers)
updated_identifiers = self.__update_pks_for_attrs(entry.get_identifiers(self.__import))
for key, value in list(updated_identifiers.items()):
if isinstance(value, dict) and "pk" in value:
del updated_identifiers[key]
@ -114,6 +166,7 @@ class FlowImporter:
pk=model_instance.pk,
)
serializer_kwargs["instance"] = model_instance
serializer_kwargs["partial"] = True
else:
self.logger.debug("initialise new instance", model=model, **updated_identifiers)
model_instance = model()
@ -121,7 +174,7 @@ class FlowImporter:
if "pk" in updated_identifiers:
model_instance.pk = updated_identifiers["pk"]
serializer_kwargs["instance"] = model_instance
full_data = self.__update_pks_for_attrs(entry.attrs)
full_data = self.__update_pks_for_attrs(entry.get_attrs(self.__import))
full_data.update(updated_identifiers)
serializer_kwargs["data"] = full_data
@ -133,7 +186,7 @@ class FlowImporter:
return serializer
def apply(self) -> bool:
"""Apply (create/update) flow json, in database transaction"""
"""Apply (create/update) models yaml, in database transaction"""
try:
with transaction.atomic():
if not self._apply_models():
@ -146,10 +199,9 @@ class FlowImporter:
return True
def _apply_models(self) -> bool:
"""Apply (create/update) flow json"""
"""Apply (create/update) models yaml"""
self.__pk_map = {}
entries = deepcopy(self.__import.entries)
for entry in entries:
for entry in self.__import.entries:
model_app_label, model_name = entry.model.split(".")
try:
model: SerializerModel = apps.get_model(model_app_label, model_name)
@ -162,23 +214,32 @@ class FlowImporter:
try:
serializer = self._validate_single(entry)
except EntryInvalidError as exc:
self.logger.warning("entry not valid", entry=entry, error=exc)
self.logger.warning("entry invalid", entry=entry, error=exc)
return False
model = serializer.save()
self.__pk_map[entry.identifiers["pk"]] = model.pk
if "pk" in entry.identifiers:
self.__pk_map[entry.identifiers["pk"]] = model.pk
entry._instance = model
self.logger.debug("updated model", model=model, pk=model.pk)
return True
def validate(self) -> bool:
"""Validate loaded flow export, ensure all models are allowed
def validate(self) -> tuple[bool, list[EventDict]]:
"""Validate loaded blueprint export, ensure all models are allowed
and serializers have no errors"""
self.logger.debug("Starting flow import validation")
self.logger.debug("Starting blueprint import validation")
orig_import = deepcopy(self.__import)
if self.__import.version != 1:
self.logger.warning("Invalid bundle version")
return False
with transaction_rollback():
return False, []
with (
transaction_rollback(),
capture_logs() as logs,
):
successful = self._apply_models()
if not successful:
self.logger.debug("Flow validation failed")
return successful
self.logger.debug("blueprint validation failed")
for log in logs:
self.logger.debug(**log)
self.__import = orig_import
return successful, logs

View File

@ -0,0 +1,4 @@
"""Blueprint labels"""
LABEL_AUTHENTIK_SYSTEM = "blueprints.goauthentik.io/system"
LABEL_AUTHENTIK_INSTANTIATE = "blueprints.goauthentik.io/instantiate"

View File

@ -0,0 +1,145 @@
"""v1 blueprints tasks"""
from dataclasses import asdict, dataclass, field
from hashlib import sha512
from pathlib import Path
from typing import Optional
from dacite import from_dict
from django.db import DatabaseError, InternalError, ProgrammingError
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from yaml import load
from yaml.error import YAMLError
from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata
from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_INSTANTIATE
from authentik.events.monitored_tasks import (
MonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
from authentik.lib.config import CONFIG
from authentik.root.celery import CELERY_APP
@dataclass
class BlueprintFile:
"""Basic info about a blueprint file"""
path: str
version: int
hash: str
last_m: int
meta: Optional[BlueprintMetadata] = field(default=None)
@CELERY_APP.task(
throws=(DatabaseError, ProgrammingError, InternalError),
)
def blueprints_find():
"""Find blueprints and return valid ones"""
blueprints = []
root = Path(CONFIG.y("blueprints_dir"))
for file in root.glob("**/*.yaml"):
path = Path(file)
with open(path, "r", encoding="utf-8") as blueprint_file:
try:
raw_blueprint = load(blueprint_file.read(), BlueprintLoader)
except YAMLError:
raw_blueprint = None
if not raw_blueprint:
continue
metadata = raw_blueprint.get("metadata", None)
version = raw_blueprint.get("version", 1)
if version != 1:
continue
file_hash = sha512(path.read_bytes()).hexdigest()
blueprint = BlueprintFile(path.relative_to(root), version, file_hash, path.stat().st_mtime)
blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
blueprints.append(blueprint)
return blueprints
@CELERY_APP.task(
throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True
)
@prefill_task
def blueprints_discover(self: MonitoredTask):
"""Find blueprints and check if they need to be created in the database"""
count = 0
for blueprint in blueprints_find():
check_blueprint_v1_file(blueprint)
count += 1
self.set_status(
TaskResult(
TaskResultStatus.SUCCESSFUL,
messages=[_("Successfully imported %(count)d files." % {"count": count})],
)
)
def check_blueprint_v1_file(blueprint: BlueprintFile):
"""Check if blueprint should be imported"""
instance: BlueprintInstance = BlueprintInstance.objects.filter(path=blueprint.path).first()
if (
blueprint.meta
and blueprint.meta.labels.get(LABEL_AUTHENTIK_INSTANTIATE, "").lower() == "false"
):
return
if not instance:
instance = BlueprintInstance(
name=blueprint.meta.name if blueprint.meta else str(blueprint.path),
path=blueprint.path,
context={},
status=BlueprintInstanceStatus.UNKNOWN,
enabled=True,
managed_models=[],
metadata={},
)
instance.save()
if instance.last_applied_hash != blueprint.hash:
instance.metadata = asdict(blueprint.meta) if blueprint.meta else {}
instance.save()
apply_blueprint.delay(instance.pk.hex)
@CELERY_APP.task(
bind=True,
base=MonitoredTask,
)
def apply_blueprint(self: MonitoredTask, instance_pk: str):
"""Apply single blueprint"""
self.set_uid(instance_pk)
self.save_on_success = False
try:
instance: BlueprintInstance = BlueprintInstance.objects.filter(pk=instance_pk).first()
if not instance or not instance.enabled:
return
full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(instance.path))
file_hash = sha512(full_path.read_bytes()).hexdigest()
with open(full_path, "r", encoding="utf-8") as blueprint_file:
importer = Importer(blueprint_file.read(), instance.context)
valid, logs = importer.validate()
if not valid:
instance.status = BlueprintInstanceStatus.ERROR
instance.save()
self.set_status(TaskResult(TaskResultStatus.ERROR, [x["event"] for x in logs]))
return
applied = importer.apply()
if not applied:
instance.status = BlueprintInstanceStatus.ERROR
instance.save()
self.set_status(TaskResult(TaskResultStatus.ERROR, "Failed to apply"))
return
instance.status = BlueprintInstanceStatus.SUCCESSFUL
instance.last_applied_hash = file_hash
instance.last_applied = now()
instance.save()
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
except (DatabaseError, ProgrammingError, InternalError, IOError) as exc:
instance.status = BlueprintInstanceStatus.ERROR
instance.save()
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))

View File

@ -98,7 +98,6 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
"group",
]
lookup_field = "slug"
filterset_fields = ["name", "slug"]
ordering = ["name"]
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:

View File

@ -1,9 +1,10 @@
"""Authenticator Devices API Views"""
from django_otp import devices_for_user
from django_otp import device_classes, devices_for_user
from django_otp.models import Device
from drf_spectacular.utils import extend_schema
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
from rest_framework.permissions import IsAuthenticated
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework.fields import BooleanField, CharField, IntegerField, SerializerMethodField
from rest_framework.permissions import IsAdminUser, IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
@ -17,6 +18,7 @@ class DeviceSerializer(MetaNameSerializer):
pk = IntegerField()
name = CharField()
type = SerializerMethodField()
confirmed = BooleanField()
def get_type(self, instance: Device) -> str:
"""Get type of device"""
@ -34,3 +36,33 @@ class DeviceViewSet(ViewSet):
"""Get all devices for current user"""
devices = devices_for_user(request.user)
return Response(DeviceSerializer(devices, many=True).data)
class AdminDeviceViewSet(ViewSet):
"""Viewset for authenticator devices"""
serializer_class = DeviceSerializer
permission_classes = [IsAdminUser]
def get_devices(self, **kwargs):
"""Get all devices in all child classes"""
for model in device_classes():
device_set = model.objects.filter(**kwargs)
yield from device_set
@extend_schema(
parameters=[
OpenApiParameter(
name="user",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.INT,
)
],
responses={200: DeviceSerializer(many=True)},
)
def list(self, request: Request) -> Response:
"""Get all devices for current user"""
kwargs = {}
if "user" in request.query_params:
kwargs = {"user": request.query_params["user"]}
return Response(DeviceSerializer(self.get_devices(**kwargs), many=True).data)

View File

@ -62,6 +62,11 @@ class GroupSerializer(ModelSerializer):
"attributes",
"users_obj",
]
extra_kwargs = {
"users": {
"default": list,
}
}
class GroupFilter(FilterSet):

View File

@ -14,12 +14,12 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.api.decorators import permission_required
from authentik.blueprints.api import ManagedSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer
from authentik.core.expression import PropertyMappingEvaluator
from authentik.core.models import PropertyMapping
from authentik.lib.utils.reflection import all_subclasses
from authentik.managed.api import ManagedSerializer
from authentik.policies.api.exec import PolicyTestSerializer

View File

@ -15,13 +15,13 @@ from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.api.decorators import permission_required
from authentik.blueprints.api import ManagedSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents
from authentik.events.models import Event, EventAction
from authentik.events.utils import model_to_dict
from authentik.managed.api import ManagedSerializer
class TokenSerializer(ManagedSerializer, ModelSerializer):

View File

@ -1,22 +1,37 @@
"""authentik core app config"""
from importlib import import_module
from django.apps import AppConfig
from django.conf import settings
from authentik.blueprints.manager import ManagedAppConfig
class AuthentikCoreConfig(AppConfig):
class AuthentikCoreConfig(ManagedAppConfig):
"""authentik core app config"""
name = "authentik.core"
label = "authentik_core"
verbose_name = "authentik Core"
mountpoint = ""
default = True
def ready(self):
import_module("authentik.core.signals")
import_module("authentik.core.managed")
def reconcile_load_core_signals(self):
"""Load core signals"""
self.import_module("authentik.core.signals")
def reconcile_debug_worker_hook(self):
"""Dispatch startup tasks inline when debugging"""
if settings.DEBUG:
from authentik.root.celery import worker_ready_hook
worker_ready_hook()
def reconcile_source_inbuilt(self):
"""Reconcile inbuilt source"""
from authentik.core.models import Source
Source.objects.update_or_create(
defaults={
"name": "authentik Built-in",
"slug": "authentik-built-in",
},
managed="goauthentik.io/sources/inbuilt",
)

View File

@ -1,17 +0,0 @@
"""Core managed objects"""
from authentik.core.models import Source
from authentik.managed.manager import EnsureExists, ObjectManager
class CoreManager(ObjectManager):
"""Core managed objects"""
def reconcile(self):
return [
EnsureExists(
Source,
"goauthentik.io/sources/inbuilt",
name="authentik Built-in",
slug="authentik-built-in",
),
]

View File

@ -4,7 +4,7 @@ from django.core.management.base import BaseCommand
from authentik.root.celery import _get_startup_tasks
class Command(BaseCommand): # pragma: no cover
class Command(BaseCommand):
"""Run bootstrap tasks to ensure certain objects are created"""
def handle(self, **options):

View File

@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand, no_translations
from guardian.management import create_anonymous_user
class Command(BaseCommand): # pragma: no cover
class Command(BaseCommand):
"""Repair missing permissions"""
@no_translations

View File

@ -22,7 +22,7 @@ BANNER_TEXT = """### authentik shell ({authentik})
)
class Command(BaseCommand): # pragma: no cover
class Command(BaseCommand):
"""Start the Django shell with all authentik models already imported"""
django_models = {}
@ -40,9 +40,6 @@ class Command(BaseCommand): # pragma: no cover
# Gather Django models and constants from each app
for app in apps.get_app_configs():
if not app.name.startswith("authentik"):
continue
# Load models from each app
for model in app.get_models():
namespace[model.__name__] = model

View File

@ -1,19 +1,22 @@
"""authentik admin Middleware to impersonate users"""
from logging import Logger
from threading import local
from contextvars import ContextVar
from typing import Callable
from uuid import uuid4
from django.http import HttpRequest, HttpResponse
from sentry_sdk.api import set_tag
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
SESSION_KEY_IMPERSONATE_USER = "authentik/impersonate/user"
SESSION_KEY_IMPERSONATE_ORIGINAL_USER = "authentik/impersonate/original_user"
LOCAL = local()
RESPONSE_HEADER_ID = "X-authentik-id"
KEY_AUTH_VIA = "auth_via"
KEY_USER = "user"
CTX_REQUEST_ID = ContextVar(STRUCTLOG_KEY_PREFIX + "request_id", default=None)
CTX_HOST = ContextVar(STRUCTLOG_KEY_PREFIX + "host", default=None)
CTX_AUTH_VIA = ContextVar(STRUCTLOG_KEY_PREFIX + KEY_AUTH_VIA, default=None)
class ImpersonateMiddleware:
"""Middleware to impersonate users"""
@ -47,26 +50,20 @@ class RequestIDMiddleware:
if not hasattr(request, "request_id"):
request_id = uuid4().hex
setattr(request, "request_id", request_id)
LOCAL.authentik = {
"request_id": request_id,
"host": request.get_host(),
}
CTX_REQUEST_ID.set(request_id)
CTX_HOST.set(request.get_host())
set_tag("authentik.request_id", request_id)
if hasattr(request, "user") and getattr(request.user, "is_authenticated", False):
CTX_AUTH_VIA.set("session")
else:
CTX_AUTH_VIA.set("unauthenticated")
response = self.get_response(request)
response[RESPONSE_HEADER_ID] = request.request_id
setattr(response, "ak_context", {})
response.ak_context.update(LOCAL.authentik)
response.ak_context["request_id"] = CTX_REQUEST_ID.get()
response.ak_context["host"] = CTX_HOST.get()
response.ak_context[KEY_AUTH_VIA] = CTX_AUTH_VIA.get()
response.ak_context[KEY_USER] = request.user.username
for key in list(LOCAL.authentik.keys()):
del LOCAL.authentik[key]
return response
# pylint: disable=unused-argument
def structlog_add_request_id(logger: Logger, method_name: str, event_dict: dict):
"""If threadlocal has authentik defined, add request_id to log"""
if hasattr(LOCAL, "authentik"):
event_dict.update(LOCAL.authentik)
if hasattr(LOCAL, "authentik_task"):
event_dict.update(LOCAL.authentik_task)
return event_dict

View File

@ -0,0 +1,26 @@
# Generated by Django 4.0.6 on 2022-08-05 22:01
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0021_source_user_path_user_path"),
]
operations = [
migrations.AlterField(
model_name="group",
name="parent",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="children",
to="authentik_core.group",
),
),
]

View File

@ -23,14 +23,14 @@ from model_utils.managers import InheritanceManager
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.blueprints.models import ManagedModel
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.signals import password_changed
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.lib.config import CONFIG
from authentik.lib.config import CONFIG, get_path_from_dict
from authentik.lib.generators import generate_id
from authentik.lib.models import CreatedUpdatedModel, DomainlessURLValidator, SerializerModel
from authentik.lib.utils.http import get_client_ip
from authentik.managed.models import ManagedModel
from authentik.policies.models import PolicyBindingModel
LOGGER = get_logger()
@ -68,7 +68,7 @@ def default_token_key():
return generate_id(int(CONFIG.y("default_token_length")))
class Group(models.Model):
class Group(SerializerModel):
"""Custom Group model which supports a basic hierarchy"""
group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@ -82,11 +82,18 @@ class Group(models.Model):
"Group",
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
related_name="children",
)
attributes = models.JSONField(default=dict, blank=True)
@property
def serializer(self) -> Serializer:
from authentik.core.api.groups import GroupSerializer
return GroupSerializer
@property
def num_pk(self) -> int:
"""Get a numerical, int32 ID for the group"""
@ -139,7 +146,7 @@ class UserManager(DjangoUserManager):
return self._create_user(username, email, password, **extra_fields)
class User(GuardianUserMixin, AbstractUser):
class User(SerializerModel, GuardianUserMixin, AbstractUser):
"""Custom User model to allow easier adding of user-based settings"""
uuid = models.UUIDField(default=uuid4, editable=False)
@ -170,6 +177,12 @@ class User(GuardianUserMixin, AbstractUser):
always_merger.merge(final_attributes, self.attributes)
return final_attributes
@property
def serializer(self) -> Serializer:
from authentik.core.api.users import UserSerializer
return UserSerializer
@cached_property
def is_superuser(self) -> bool:
"""Get supseruser status based on membership in a group with superuser status"""
@ -213,6 +226,8 @@ class User(GuardianUserMixin, AbstractUser):
mode: str = CONFIG.y("avatars", "none")
if mode == "none":
return DEFAULT_AVATAR
if mode.startswith("attributes."):
return get_path_from_dict(self.attributes, mode[11:], default=DEFAULT_AVATAR)
# gravatar uses md5 for their URLs, so md5 can't be avoided
mail_hash = md5(self.email.lower().encode("utf-8")).hexdigest() # nosec
if mode == "gravatar":
@ -274,7 +289,7 @@ class Provider(SerializerModel):
return self.name
class Application(PolicyBindingModel):
class Application(SerializerModel, PolicyBindingModel):
"""Every Application which uses authentik for authentication/identification/authorization
needs an Application record. Other authentication types can subclass this Model to
add custom fields and other properties"""
@ -305,6 +320,12 @@ class Application(PolicyBindingModel):
meta_description = models.TextField(default="", blank=True)
meta_publisher = models.TextField(default="", blank=True)
@property
def serializer(self) -> Serializer:
from authentik.core.api.applications import ApplicationSerializer
return ApplicationSerializer
@property
def get_meta_icon(self) -> Optional[str]:
"""Get the URL to the App Icon image. If the name is /static or starts with http
@ -452,7 +473,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
return self.name
class UserSourceConnection(CreatedUpdatedModel):
class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
"""Connection between User and Source."""
user = models.ForeignKey(User, on_delete=models.CASCADE)
@ -460,6 +481,11 @@ class UserSourceConnection(CreatedUpdatedModel):
objects = InheritanceManager()
@property
def serializer(self) -> type[Serializer]:
"""Get serializer for this model"""
raise NotImplementedError
class Meta:
unique_together = (("user", "source"),)
@ -482,8 +508,9 @@ class ExpiringModel(models.Model):
def filter_not_expired(cls, **kwargs) -> QuerySet:
"""Filer for tokens which are not expired yet or are not expiring,
and match filters in `kwargs`"""
expired = Q(expires__lt=now(), expiring=True)
return cls.objects.exclude(expired).filter(**kwargs)
for obj in cls.objects.filter(**kwargs).filter(Q(expires__lt=now(), expiring=True)):
obj.delete()
return cls.objects.filter(**kwargs)
@property
def is_expired(self) -> bool:
@ -513,7 +540,7 @@ class TokenIntents(models.TextChoices):
INTENT_APP_PASSWORD = "app_password" # nosec
class Token(ManagedModel, ExpiringModel):
class Token(SerializerModel, ManagedModel, ExpiringModel):
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""
token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@ -525,6 +552,12 @@ class Token(ManagedModel, ExpiringModel):
user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+")
description = models.TextField(default="", blank=True)
@property
def serializer(self) -> type[Serializer]:
from authentik.core.api.tokens import TokenSerializer
return TokenSerializer
def expire_action(self, *args, **kwargs):
"""Handler which is called when this object is expired."""
from authentik.events.models import Event, EventAction

View File

@ -7,6 +7,12 @@
<script src="{% static 'dist/admin/AdminInterface.js' %}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<script>
window.authentik = {};
window.authentik.locale = "{{ tenant.default_locale }}";
window.authentik.config = JSON.parse('{{ config_json|safe }}');
window.authentik.tenant = JSON.parse('{{ tenant_json|safe }}');
</script>
{% endblock %}
{% block body %}

View File

@ -11,6 +11,9 @@
{% endif %}
<script>
window.authentik = {};
window.authentik.locale = "{{ tenant.default_locale }}";
window.authentik.config = JSON.parse( '{{ config_json|safe }}');
window.authentik.tenant = JSON.parse('{{ tenant_json|safe }}');
window.authentik.flow = {
"layout": "{{ flow.layout }}",
};

View File

@ -7,6 +7,12 @@
<script src="{% static 'dist/user/UserInterface.js' %}" type="module"></script>
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
<script>
window.authentik = {};
window.authentik.locale = "{{ tenant.default_locale }}";
window.authentik.config = JSON.parse('{{ config_json|safe }}');
window.authentik.tenant = JSON.parse('{{ tenant_json|safe }}');
</script>
{% endblock %}
{% block body %}

View File

@ -1,10 +1,13 @@
"""Test Users API"""
from json import loads
from django.urls.base import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
from authentik.flows.models import FlowDesignation
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id, generate_key
from authentik.stages.email.models import EmailStage
from authentik.tenants.models import Tenant
@ -211,3 +214,47 @@ class TestUsersAPI(APITestCase):
self.assertJSONEqual(
response.content.decode(), {"path": ["No empty segments in user path allowed."]}
)
def test_me(self):
"""Test user's me endpoint"""
self.client.force_login(self.admin)
response = self.client.get(reverse("authentik_api:user-me"))
self.assertEqual(response.status_code, 200)
@CONFIG.patch("avatars", "none")
def test_avatars_none(self):
"""Test avatars none"""
self.client.force_login(self.admin)
response = self.client.get(reverse("authentik_api:user-me"))
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
self.assertEqual(body["user"]["avatar"], "/static/dist/assets/images/user_default.png")
@CONFIG.patch("avatars", "gravatar")
def test_avatars_gravatar(self):
"""Test avatars gravatar"""
self.client.force_login(self.admin)
response = self.client.get(reverse("authentik_api:user-me"))
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
self.assertIn("gravatar", body["user"]["avatar"])
@CONFIG.patch("avatars", "foo-%(username)s")
def test_avatars_custom(self):
"""Test avatars custom"""
self.client.force_login(self.admin)
response = self.client.get(reverse("authentik_api:user-me"))
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
self.assertEqual(body["user"]["avatar"], f"foo-{self.admin.username}")
@CONFIG.patch("avatars", "attributes.foo.avatar")
def test_avatars_attributes(self):
"""Test avatars attributes"""
self.admin.attributes = {"foo": {"avatar": "bar"}}
self.admin.save()
self.client.force_login(self.admin)
response = self.client.get(reverse("authentik_api:user-me"))
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
self.assertEqual(body["user"]["avatar"], "bar")

View File

@ -4,11 +4,10 @@ from django.contrib.auth.decorators import login_required
from django.urls import path
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import RedirectView
from django.views.generic.base import TemplateView
from authentik.core.views import apps, impersonate
from authentik.core.views.debug import AccessDeniedView
from authentik.core.views.interface import FlowInterfaceView
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
from authentik.core.views.session import EndSessionView
urlpatterns = [
@ -39,12 +38,12 @@ urlpatterns = [
# Interfaces
path(
"if/admin/",
ensure_csrf_cookie(TemplateView.as_view(template_name="if/admin.html")),
ensure_csrf_cookie(InterfaceView.as_view(template_name="if/admin.html")),
name="if-admin",
),
path(
"if/user/",
ensure_csrf_cookie(TemplateView.as_view(template_name="if/user.html")),
ensure_csrf_cookie(InterfaceView.as_view(template_name="if/user.html")),
name="if-user",
),
path(
@ -58,10 +57,10 @@ urlpatterns = [
name="if-session-end",
),
# Fallback for WS
path("ws/outpost/<uuid:pk>/", TemplateView.as_view(template_name="if/admin.html")),
path("ws/outpost/<uuid:pk>/", InterfaceView.as_view(template_name="if/admin.html")),
path(
"ws/client/",
TemplateView.as_view(template_name="if/admin.html"),
InterfaceView.as_view(template_name="if/admin.html"),
),
]

View File

@ -1,13 +1,26 @@
"""Interface views"""
from json import dumps
from typing import Any
from django.shortcuts import get_object_or_404
from django.views.generic.base import TemplateView
from rest_framework.request import Request
from authentik.api.v3.config import ConfigView
from authentik.flows.models import Flow
from authentik.tenants.api import CurrentTenantSerializer
class FlowInterfaceView(TemplateView):
class InterfaceView(TemplateView):
"""Base interface view"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["config_json"] = dumps(ConfigView(request=Request(self.request)).get_config().data)
kwargs["tenant_json"] = dumps(CurrentTenantSerializer(self.request.tenant).data)
return super().get_context_data(**kwargs)
class FlowInterfaceView(InterfaceView):
"""Flow interface"""
template_name = "if/flow.html"

View File

@ -22,8 +22,8 @@ from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.apps import MANAGED_KEY
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.managed import MANAGED_KEY
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction

View File

@ -1,16 +1,72 @@
"""authentik crypto app config"""
from importlib import import_module
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from django.apps import AppConfig
from authentik.blueprints.manager import ManagedAppConfig
from authentik.lib.generators import generate_id
if TYPE_CHECKING:
from authentik.crypto.models import CertificateKeyPair
MANAGED_KEY = "goauthentik.io/crypto/jwt-managed"
class AuthentikCryptoConfig(AppConfig):
class AuthentikCryptoConfig(ManagedAppConfig):
"""authentik crypto app config"""
name = "authentik.crypto"
label = "authentik_crypto"
verbose_name = "authentik Crypto"
default = True
def ready(self):
import_module("authentik.crypto.managed")
import_module("authentik.crypto.tasks")
def reconcile_load_crypto_tasks(self):
"""Load crypto tasks"""
self.import_module("authentik.crypto.tasks")
def _create_update_cert(self, cert: Optional["CertificateKeyPair"] = None):
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair
builder = CertificateBuilder()
builder.common_name = "goauthentik.io"
builder.build(
subject_alt_names=["goauthentik.io"],
validity_days=360,
)
if not cert:
cert = CertificateKeyPair()
cert.certificate_data = builder.certificate
cert.key_data = builder.private_key
cert.name = "authentik Internal JWT Certificate"
cert.managed = MANAGED_KEY
cert.save()
def reconcile_managed_jwt_cert(self):
"""Ensure managed JWT certificate"""
from authentik.crypto.models import CertificateKeyPair
certs = CertificateKeyPair.objects.filter(managed=MANAGED_KEY)
if not certs.exists():
self._create_update_cert()
return
cert: CertificateKeyPair = certs.first()
now = datetime.now()
if now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after:
self._create_update_cert(cert)
def reconcile_self_signed(self):
"""Create self-signed keypair"""
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair
name = "authentik Self-signed Certificate"
if CertificateKeyPair.objects.filter(name=name).exists():
return
builder = CertificateBuilder()
builder.build(subject_alt_names=[f"{generate_id()}.self-signed.goauthentik.io"])
CertificateKeyPair.objects.create(
name="authentik Self-signed Certificate",
certificate_data=builder.certificate,
key_data=builder.private_key,
)

View File

@ -1,40 +0,0 @@
"""Crypto managed objects"""
from datetime import datetime
from typing import Optional
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair
from authentik.managed.manager import ObjectManager
MANAGED_KEY = "goauthentik.io/crypto/jwt-managed"
class CryptoManager(ObjectManager):
"""Crypto managed objects"""
def _create(self, cert: Optional[CertificateKeyPair] = None):
builder = CertificateBuilder()
builder.common_name = "goauthentik.io"
builder.build(
subject_alt_names=["goauthentik.io"],
validity_days=360,
)
if not cert:
cert = CertificateKeyPair()
cert.certificate_data = builder.certificate
cert.key_data = builder.private_key
cert.name = "authentik Internal JWT Certificate"
cert.managed = MANAGED_KEY
cert.save()
def reconcile(self):
certs = CertificateKeyPair.objects.filter(managed=MANAGED_KEY)
if not certs.exists():
self._create()
return []
cert: CertificateKeyPair = certs.first()
now = datetime.now()
if now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after:
self._create(cert)
return []
return []

View File

@ -5,24 +5,10 @@ from django.db import migrations
from authentik.lib.generators import generate_id
def create_self_signed(apps, schema_editor):
CertificateKeyPair = apps.get_model("authentik_crypto", "CertificateKeyPair")
db_alias = schema_editor.connection.alias
from authentik.crypto.builder import CertificateBuilder
builder = CertificateBuilder()
builder.build(subject_alt_names=[f"{generate_id()}.self-signed.goauthentik.io"])
CertificateKeyPair.objects.using(db_alias).create(
name="authentik Self-signed Certificate",
certificate_data=builder.certificate,
key_data=builder.private_key,
)
class Migration(migrations.Migration):
dependencies = [
("authentik_crypto", "0001_initial"),
]
operations = [migrations.RunPython(create_self_signed)]
operations = []

View File

@ -16,15 +16,16 @@ from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import Certificate, load_pem_x509_certificate
from django.db import models
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.lib.models import CreatedUpdatedModel
from authentik.managed.models import ManagedModel
from authentik.blueprints.models import ManagedModel
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
LOGGER = get_logger()
class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
"""CertificateKeyPair that can be used for signing or encrypting if `key_data`
is set, otherwise it can be used to verify remote data."""
@ -44,6 +45,12 @@ class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
_private_key: Optional[RSAPrivateKey | EllipticCurvePrivateKey | Ed25519PrivateKey] = None
_public_key: Optional[RSAPublicKey | EllipticCurvePublicKey | Ed25519PublicKey] = None
@property
def serializer(self) -> Serializer:
from authentik.crypto.api import CertificateKeyPairSerializer
return CertificateKeyPairSerializer
@property
def certificate(self) -> Certificate:
"""Get python cryptography Certificate instance"""

View File

@ -1,9 +1,8 @@
"""authentik events app"""
from importlib import import_module
from django.apps import AppConfig
from prometheus_client import Gauge
from authentik.blueprints.manager import ManagedAppConfig
GAUGE_TASKS = Gauge(
"authentik_system_tasks",
"System tasks and their status",
@ -11,12 +10,14 @@ GAUGE_TASKS = Gauge(
)
class AuthentikEventsConfig(AppConfig):
class AuthentikEventsConfig(ManagedAppConfig):
"""authentik events app"""
name = "authentik.events"
label = "authentik_events"
verbose_name = "authentik Events"
default = True
def ready(self):
import_module("authentik.events.signals")
def reconcile_load_events_signals(self):
"""Load events signals"""
self.import_module("authentik.events.signals")

View File

@ -11,7 +11,6 @@ from django.http import HttpRequest, HttpResponse
from django_otp.plugins.otp_static.models import StaticToken
from guardian.models import UserObjectPermission
from authentik.core.middleware import LOCAL
from authentik.core.models import AuthenticatedSession, User
from authentik.events.models import Event, EventAction, Notification
from authentik.events.signals import EventNewThread
@ -45,36 +44,46 @@ class AuditMiddleware:
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response
def connect(self, request: HttpRequest):
"""Connect signal for automatic logging"""
if not hasattr(request, "user"):
return
if not getattr(request.user, "is_authenticated", False):
return
if not hasattr(request, "request_id"):
return
post_save_handler = partial(self.post_save_handler, user=request.user, request=request)
pre_delete_handler = partial(self.pre_delete_handler, user=request.user, request=request)
post_save.connect(
post_save_handler,
dispatch_uid=request.request_id,
weak=False,
)
pre_delete.connect(
pre_delete_handler,
dispatch_uid=request.request_id,
weak=False,
)
def disconnect(self, request: HttpRequest):
"""Disconnect signals"""
if not hasattr(request, "request_id"):
return
post_save.disconnect(dispatch_uid=request.request_id)
pre_delete.disconnect(dispatch_uid=request.request_id)
def __call__(self, request: HttpRequest) -> HttpResponse:
# Connect signal for automatic logging
if hasattr(request, "user") and getattr(request.user, "is_authenticated", False):
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
)
post_save.connect(
post_save_handler,
dispatch_uid=LOCAL.authentik["request_id"],
weak=False,
)
pre_delete.connect(
pre_delete_handler,
dispatch_uid=LOCAL.authentik["request_id"],
weak=False,
)
self.connect(request)
response = self.get_response(request)
post_save.disconnect(dispatch_uid=LOCAL.authentik["request_id"])
pre_delete.disconnect(dispatch_uid=LOCAL.authentik["request_id"])
self.disconnect(request)
return response
# pylint: disable=unused-argument
def process_exception(self, request: HttpRequest, exception: Exception):
"""Disconnect handlers in case of exception"""
post_save.disconnect(dispatch_uid=LOCAL.authentik["request_id"])
pre_delete.disconnect(dispatch_uid=LOCAL.authentik["request_id"])
self.disconnect(request)
if settings.DEBUG:
return

View File

@ -1,29 +1,5 @@
# Generated by Django 4.0.4 on 2022-05-30 18:08
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.events.models import TransportMode
def notify_local_transport(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
local_transport, _ = NotificationTransport.objects.using(db_alias).update_or_create(
name="default-local-transport",
defaults={"mode": TransportMode.LOCAL},
)
for trigger in NotificationRule.objects.using(db_alias).filter(
name__in=[
"default-notify-configuration-error",
"default-notify-exception",
"default-notify-update",
]
):
trigger.transports.add(local_transport)
class Migration(migrations.Migration):
@ -46,5 +22,4 @@ class Migration(migrations.Migration):
default="local",
),
),
migrations.RunPython(notify_local_transport),
]

View File

@ -1,130 +1,6 @@
# Generated by Django 3.1.4 on 2021-01-10 18:57
from django.apps.registry import Apps
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.events.models import EventAction, NotificationSeverity, TransportMode
def notify_configuration_error(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
Group = apps.get_model("authentik_core", "Group")
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
EventMatcherPolicy = apps.get_model("authentik_policies_event_matcher", "EventMatcherPolicy")
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
admin_group = (
Group.objects.using(db_alias).filter(name="authentik Admins", is_superuser=True).first()
)
policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
name="default-match-configuration-error",
defaults={"action": EventAction.CONFIGURATION_ERROR},
)
trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
name="default-notify-configuration-error",
defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
)
trigger.transports.set(
NotificationTransport.objects.using(db_alias).filter(name="default-email-transport")
)
trigger.save()
PolicyBinding.objects.using(db_alias).update_or_create(
target=trigger,
policy=policy,
defaults={
"order": 0,
},
)
def notify_update(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
Group = apps.get_model("authentik_core", "Group")
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
EventMatcherPolicy = apps.get_model("authentik_policies_event_matcher", "EventMatcherPolicy")
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
admin_group = (
Group.objects.using(db_alias).filter(name="authentik Admins", is_superuser=True).first()
)
policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
name="default-match-update",
defaults={"action": EventAction.UPDATE_AVAILABLE},
)
trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
name="default-notify-update",
defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
)
trigger.transports.set(
NotificationTransport.objects.using(db_alias).filter(name="default-email-transport")
)
trigger.save()
PolicyBinding.objects.using(db_alias).update_or_create(
target=trigger,
policy=policy,
defaults={
"order": 0,
},
)
def notify_exception(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
Group = apps.get_model("authentik_core", "Group")
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
EventMatcherPolicy = apps.get_model("authentik_policies_event_matcher", "EventMatcherPolicy")
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
admin_group = (
Group.objects.using(db_alias).filter(name="authentik Admins", is_superuser=True).first()
)
policy_policy_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
name="default-match-policy-exception",
defaults={"action": EventAction.POLICY_EXCEPTION},
)
policy_pm_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
name="default-match-property-mapping-exception",
defaults={"action": EventAction.PROPERTY_MAPPING_EXCEPTION},
)
trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
name="default-notify-exception",
defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
)
trigger.transports.set(
NotificationTransport.objects.using(db_alias).filter(name="default-email-transport")
)
trigger.save()
PolicyBinding.objects.using(db_alias).update_or_create(
target=trigger,
policy=policy_policy_exc,
defaults={
"order": 0,
},
)
PolicyBinding.objects.using(db_alias).update_or_create(
target=trigger,
policy=policy_pm_exc,
defaults={
"order": 1,
},
)
def transport_email_global(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
NotificationTransport.objects.using(db_alias).update_or_create(
name="default-email-transport",
defaults={"mode": TransportMode.EMAIL},
)
class Migration(migrations.Migration):
@ -134,14 +10,6 @@ class Migration(migrations.Migration):
"authentik_events",
"0010_notification_notificationtransport_notificationrule",
),
("authentik_core", "0016_auto_20201202_2234"),
("authentik_policies_event_matcher", "0003_auto_20210110_1907"),
("authentik_policies", "0004_policy_execution_logging"),
]
operations = [
migrations.RunPython(transport_email_global),
migrations.RunPython(notify_configuration_error),
migrations.RunPython(notify_update),
migrations.RunPython(notify_exception),
]
operations = []

View File

@ -30,7 +30,7 @@ from authentik.core.middleware import (
from authentik.core.models import ExpiringModel, Group, PropertyMapping, User
from authentik.events.geo import GEOIP_READER
from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict
from authentik.lib.models import DomainlessURLValidator
from authentik.lib.models import DomainlessURLValidator, SerializerModel
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.http import get_client_ip, get_http_session
from authentik.lib.utils.time import timedelta_from_string
@ -168,7 +168,7 @@ class EventManager(Manager):
return self.get_queryset().get_events_per_day()
class Event(ExpiringModel):
class Event(SerializerModel, ExpiringModel):
"""An individual Audit/Metrics/Notification/Error Event"""
event_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@ -273,6 +273,12 @@ class Event(ExpiringModel):
)
super().save(*args, **kwargs)
@property
def serializer(self) -> "Serializer":
from authentik.events.api.events import EventSerializer
return EventSerializer
@property
def summary(self) -> str:
"""Return a summary of this event."""
@ -298,7 +304,7 @@ class TransportMode(models.TextChoices):
EMAIL = "email", _("Email")
class NotificationTransport(models.Model):
class NotificationTransport(SerializerModel):
"""Action which is executed when a Rule matches"""
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@ -448,6 +454,12 @@ class NotificationTransport(models.Model):
except (SMTPException, ConnectionError, OSError) as exc:
raise NotificationTransportError from exc
@property
def serializer(self) -> "Serializer":
from authentik.events.api.notification_transports import NotificationTransportSerializer
return NotificationTransportSerializer
def __str__(self) -> str:
return f"Notification Transport {self.name}"
@ -465,7 +477,7 @@ class NotificationSeverity(models.TextChoices):
ALERT = "alert", _("Alert")
class Notification(models.Model):
class Notification(SerializerModel):
"""Event Notification"""
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@ -476,6 +488,12 @@ class Notification(models.Model):
seen = models.BooleanField(default=False)
user = models.ForeignKey(User, on_delete=models.CASCADE)
@property
def serializer(self) -> "Serializer":
from authentik.events.api.notifications import NotificationSerializer
return NotificationSerializer
def __str__(self) -> str:
body_trunc = (self.body[:75] + "..") if len(self.body) > 75 else self.body
return f"Notification for user {self.user}: {body_trunc}"
@ -486,7 +504,7 @@ class Notification(models.Model):
verbose_name_plural = _("Notifications")
class NotificationRule(PolicyBindingModel):
class NotificationRule(SerializerModel, PolicyBindingModel):
"""Decide when to create a Notification based on policies attached to this object."""
name = models.TextField(unique=True)
@ -518,6 +536,12 @@ class NotificationRule(PolicyBindingModel):
on_delete=models.SET_NULL,
)
@property
def serializer(self) -> "Serializer":
from authentik.events.api.notification_rules import NotificationRuleSerializer
return NotificationRuleSerializer
def __str__(self) -> str:
return f"Notification Rule {self.name}"

View File

@ -1,6 +1,7 @@
"""event utilities"""
import re
from dataclasses import asdict, is_dataclass
from pathlib import Path
from typing import Any, Optional
from uuid import UUID
@ -97,6 +98,8 @@ def sanitize_dict(source: dict[Any, Any]) -> dict[Any, Any]:
continue
elif isinstance(value, City):
final_dict[key] = GEOIP_READER.city_to_dict(value)
elif isinstance(value, Path):
final_dict[key] = str(value)
elif isinstance(value, type):
final_dict[key] = {
"type": value.__name__,

View File

@ -3,7 +3,8 @@ from dataclasses import dataclass
from django.core.cache import cache
from django.db.models import Model
from django.http.response import HttpResponseBadRequest, JsonResponse
from django.http import HttpResponse
from django.http.response import HttpResponseBadRequest
from django.urls import reverse
from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
@ -19,6 +20,8 @@ from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.blueprints.v1.exporter import Exporter
from authentik.blueprints.v1.importer import Importer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
CacheSerializer,
@ -29,9 +32,6 @@ from authentik.core.api.utils import (
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
from authentik.flows.transfer.common import DataclassEncoder
from authentik.flows.transfer.exporter import FlowExporter
from authentik.flows.transfer.importer import FlowImporter
from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
from authentik.lib.views import bad_request_message
@ -163,12 +163,13 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
)
@action(detail=False, methods=["POST"], parser_classes=(MultiPartParser,))
def import_flow(self, request: Request) -> Response:
"""Import flow from .akflow file"""
"""Import flow from .yaml file"""
file = request.FILES.get("file", None)
if not file:
return HttpResponseBadRequest()
importer = FlowImporter(file.read().decode())
valid = importer.validate()
importer = Importer(file.read().decode())
valid, _logs = importer.validate()
# TODO: return logs
if not valid:
return HttpResponseBadRequest()
successful = importer.apply()
@ -195,11 +196,11 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
@action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=unused-argument
def export(self, request: Request, slug: str) -> Response:
"""Export flow to .akflow file"""
"""Export flow to .yaml file"""
flow = self.get_object()
exporter = FlowExporter(flow)
response = JsonResponse(exporter.export(), encoder=DataclassEncoder, safe=False)
response["Content-Disposition"] = f'attachment; filename="{flow.slug}.akflow"'
exporter = Exporter(flow)
response = HttpResponse(content=exporter.export_to_string())
response["Content-Disposition"] = f'attachment; filename="{flow.slug}.yaml"'
return response
@extend_schema(responses={200: FlowDiagramSerializer()})

View File

@ -1,10 +1,7 @@
"""authentik flows app config"""
from importlib import import_module
from django.apps import AppConfig
from django.db.utils import ProgrammingError
from prometheus_client import Gauge, Histogram
from authentik.blueprints.manager import ManagedAppConfig
from authentik.lib.utils.reflection import all_subclasses
GAUGE_FLOWS_CACHED = Gauge(
@ -18,20 +15,22 @@ HIST_FLOWS_PLAN_TIME = Histogram(
)
class AuthentikFlowsConfig(AppConfig):
class AuthentikFlowsConfig(ManagedAppConfig):
"""authentik flows app config"""
name = "authentik.flows"
label = "authentik_flows"
mountpoint = "flows/"
verbose_name = "authentik Flows"
default = True
def ready(self):
import_module("authentik.flows.signals")
try:
from authentik.flows.models import Stage
def reconcile_load_flows_signals(self):
"""Load flows signals"""
self.import_module("authentik.flows.signals")
for stage in all_subclasses(Stage):
_ = stage().type
except ProgrammingError:
pass
def reconcile_stages_loaded(self):
"""Ensure all stages are loaded"""
from authentik.flows.models import Stage
for stage in all_subclasses(Stage):
_ = stage().type

View File

@ -1,14 +1,16 @@
"""Challenge helpers"""
from dataclasses import asdict, is_dataclass
from enum import Enum
from typing import TYPE_CHECKING, Optional, TypedDict
from uuid import UUID
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.http import JsonResponse
from rest_framework.fields import ChoiceField, DictField
from rest_framework.serializers import CharField
from authentik.core.api.utils import PassiveSerializer
from authentik.flows.transfer.common import DataclassEncoder
if TYPE_CHECKING:
from authentik.flows.stage import StageView
@ -105,7 +107,7 @@ class PermissionDict(TypedDict):
class PermissionSerializer(PassiveSerializer):
"""Permission used for consent"""
name = CharField()
name = CharField(allow_blank=True)
id = CharField()
@ -135,6 +137,19 @@ class AutoSubmitChallengeResponse(ChallengeResponse):
component = CharField(default="ak-stage-autosubmit")
class DataclassEncoder(DjangoJSONEncoder):
"""Convert any dataclass to json"""
def default(self, o):
if is_dataclass(o):
return asdict(o)
if isinstance(o, UUID):
return str(o)
if isinstance(o, Enum):
return o.value
return super().default(o) # pragma: no cover
class HttpChallengeResponse(JsonResponse):
"""Subclass of JsonResponse that uses the `DataclassEncoder`"""

View File

@ -1,22 +0,0 @@
"""Apply flow from commandline"""
from django.core.management.base import BaseCommand, no_translations
from authentik.flows.transfer.importer import FlowImporter
class Command(BaseCommand): # pragma: no cover
"""Apply flow from commandline"""
@no_translations
def handle(self, *args, **options):
"""Apply all flows in order, abort when one fails to import"""
for flow_path in options.get("flows", []):
with open(flow_path, "r", encoding="utf8") as flow_file:
importer = FlowImporter(flow_file.read())
valid = importer.validate()
if not valid:
raise ValueError("Flow invalid")
importer.apply()
def add_arguments(self, parser):
parser.add_argument("flows", nargs="+", type=str)

View File

@ -48,7 +48,7 @@ class FlowPlanProcess(PROCESS_CLASS): # pragma: no cover
self.return_dict[self.index] = diffs
class Command(BaseCommand): # pragma: no cover
class Command(BaseCommand):
"""Benchmark authentik"""
def add_arguments(self, parser):

View File

@ -1,103 +1,12 @@
# Generated by Django 3.0.3 on 2020-05-08 14:30
from django.apps.registry import Apps
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.flows.models import FlowDesignation
from authentik.stages.identification.models import UserFields
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Flow = apps.get_model("authentik_flows", "Flow")
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
PasswordStage = apps.get_model("authentik_stages_password", "PasswordStage")
UserLoginStage = apps.get_model("authentik_stages_user_login", "UserLoginStage")
IdentificationStage = apps.get_model("authentik_stages_identification", "IdentificationStage")
db_alias = schema_editor.connection.alias
identification_stage, _ = IdentificationStage.objects.using(db_alias).update_or_create(
name="default-authentication-identification",
defaults={
"user_fields": [UserFields.E_MAIL, UserFields.USERNAME],
},
)
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
name="default-authentication-password",
defaults={"backends": [BACKEND_INBUILT, BACKEND_LDAP, BACKEND_APP_PASSWORD]},
)
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
name="default-authentication-login"
)
flow, _ = Flow.objects.using(db_alias).update_or_create(
slug="default-authentication-flow",
designation=FlowDesignation.AUTHENTICATION,
defaults={
"name": "Welcome to authentik!",
},
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow,
stage=identification_stage,
defaults={
"order": 10,
},
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow,
stage=password_stage,
defaults={
"order": 20,
},
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow,
stage=login_stage,
defaults={
"order": 100,
},
)
def create_default_invalidation_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Flow = apps.get_model("authentik_flows", "Flow")
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
UserLogoutStage = apps.get_model("authentik_stages_user_logout", "UserLogoutStage")
db_alias = schema_editor.connection.alias
UserLogoutStage.objects.using(db_alias).update_or_create(name="default-invalidation-logout")
flow, _ = Flow.objects.using(db_alias).update_or_create(
slug="default-invalidation-flow",
designation=FlowDesignation.INVALIDATION,
defaults={
"name": "Logout",
},
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow,
stage=UserLogoutStage.objects.using(db_alias).first(),
defaults={
"order": 0,
},
)
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0007_auto_20200703_2059"),
("authentik_stages_user_login", "0001_initial"),
("authentik_stages_user_logout", "0001_initial"),
("authentik_stages_password", "0001_initial"),
("authentik_stages_identification", "0001_initial"),
]
operations = [
migrations.RunPython(create_default_authentication_flow),
migrations.RunPython(create_default_invalidation_flow),
]
operations = []

View File

@ -1,151 +1,12 @@
# Generated by Django 3.0.6 on 2020-05-23 15:47
from django.apps.registry import Apps
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.flows.models import FlowDesignation
from authentik.stages.prompt.models import FieldTypes
FLOW_POLICY_EXPRESSION = """# This policy ensures that this flow can only be used when the user
# is in a SSO Flow (meaning they come from an external IdP)
return ak_is_sso_flow"""
PROMPT_POLICY_EXPRESSION = """# Check if we've not been given a username by the external IdP
# and trigger the enrollment flow
return 'username' not in context.get('prompt_data', {})"""
def create_default_source_enrollment_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Flow = apps.get_model("authentik_flows", "Flow")
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
ExpressionPolicy = apps.get_model("authentik_policies_expression", "ExpressionPolicy")
PromptStage = apps.get_model("authentik_stages_prompt", "PromptStage")
Prompt = apps.get_model("authentik_stages_prompt", "Prompt")
UserWriteStage = apps.get_model("authentik_stages_user_write", "UserWriteStage")
UserLoginStage = apps.get_model("authentik_stages_user_login", "UserLoginStage")
db_alias = schema_editor.connection.alias
# Create a policy that only allows this flow when doing an SSO Request
flow_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
name="default-source-enrollment-if-sso",
defaults={"expression": FLOW_POLICY_EXPRESSION},
)
# This creates a Flow used by sources to enroll users
# It makes sure that a username is set, and if not, prompts the user for a Username
flow, _ = Flow.objects.using(db_alias).update_or_create(
slug="default-source-enrollment",
designation=FlowDesignation.ENROLLMENT,
defaults={
"name": "Welcome to authentik! Please select a username.",
},
)
PolicyBinding.objects.using(db_alias).update_or_create(
policy=flow_policy, target=flow, defaults={"order": 0}
)
# PromptStage to ask user for their username
prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create(
name="default-source-enrollment-prompt",
)
prompt, _ = Prompt.objects.using(db_alias).update_or_create(
field_key="username",
defaults={
"label": "Username",
"type": FieldTypes.TEXT,
"required": True,
"placeholder": "Username",
"order": 100,
},
)
prompt_stage.fields.add(prompt)
# Policy to only trigger prompt when no username is given
prompt_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
name="default-source-enrollment-if-username",
defaults={"expression": PROMPT_POLICY_EXPRESSION},
)
# UserWrite stage to create the user, and login stage to log user in
user_write, _ = UserWriteStage.objects.using(db_alias).update_or_create(
name="default-source-enrollment-write"
)
user_login, _ = UserLoginStage.objects.using(db_alias).update_or_create(
name="default-source-enrollment-login"
)
binding, _ = FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow,
stage=prompt_stage,
defaults={"order": 0, "re_evaluate_policies": True},
)
PolicyBinding.objects.using(db_alias).update_or_create(
policy=prompt_policy, target=binding, defaults={"order": 0}
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow, stage=user_write, defaults={"order": 1}
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow, stage=user_login, defaults={"order": 2}
)
def create_default_source_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Flow = apps.get_model("authentik_flows", "Flow")
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
ExpressionPolicy = apps.get_model("authentik_policies_expression", "ExpressionPolicy")
UserLoginStage = apps.get_model("authentik_stages_user_login", "UserLoginStage")
db_alias = schema_editor.connection.alias
# Create a policy that only allows this flow when doing an SSO Request
flow_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
name="default-source-authentication-if-sso",
defaults={
"expression": FLOW_POLICY_EXPRESSION,
},
)
# This creates a Flow used by sources to authenticate users
flow, _ = Flow.objects.using(db_alias).update_or_create(
slug="default-source-authentication",
designation=FlowDesignation.AUTHENTICATION,
defaults={
"name": "Welcome to authentik!",
},
)
PolicyBinding.objects.using(db_alias).update_or_create(
policy=flow_policy, target=flow, defaults={"order": 0}
)
user_login, _ = UserLoginStage.objects.using(db_alias).update_or_create(
name="default-source-authentication-login"
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow, stage=user_login, defaults={"order": 0}
)
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0008_default_flows"),
("authentik_policies", "0001_initial"),
("authentik_policies_expression", "0001_initial"),
("authentik_stages_prompt", "0001_initial"),
("authentik_stages_user_write", "0001_initial"),
("authentik_stages_user_login", "0001_initial"),
]
operations = [
migrations.RunPython(create_default_source_enrollment_flow),
migrations.RunPython(create_default_source_authentication_flow),
]
operations = []

View File

@ -1,46 +1,12 @@
# Generated by Django 3.0.6 on 2020-05-24 11:34
from django.apps.registry import Apps
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.flows.models import FlowDesignation
def create_default_provider_authorization_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Flow = apps.get_model("authentik_flows", "Flow")
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
ConsentStage = apps.get_model("authentik_stages_consent", "ConsentStage")
db_alias = schema_editor.connection.alias
# Empty flow for providers where consent is implicitly given
Flow.objects.using(db_alias).update_or_create(
slug="default-provider-authorization-implicit-consent",
designation=FlowDesignation.AUTHORIZATION,
defaults={"name": "Authorize Application"},
)
# Flow with consent form to obtain explicit user consent
flow, _ = Flow.objects.using(db_alias).update_or_create(
slug="default-provider-authorization-explicit-consent",
designation=FlowDesignation.AUTHORIZATION,
defaults={"name": "Authorize Application"},
)
stage, _ = ConsentStage.objects.using(db_alias).update_or_create(
name="default-provider-authorization-consent"
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow, stage=stage, defaults={"order": 0}
)
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0009_source_flows"),
("authentik_stages_consent", "0001_initial"),
]
operations = [migrations.RunPython(create_default_provider_authorization_flow)]
operations = []

View File

@ -1,27 +1,5 @@
# Generated by Django 3.1 on 2020-08-28 13:14
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def add_title_for_defaults(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
slug_title_map = {
"default-authentication-flow": "Welcome to authentik!",
"default-invalidation-flow": "Default Invalidation Flow",
"default-source-enrollment": "Welcome to authentik! Please select a username.",
"default-source-authentication": "Welcome to authentik!",
"default-provider-authorization-implicit-consent": "Redirecting to %(app)s",
"default-provider-authorization-explicit-consent": "Redirecting to %(app)s",
"default-password-change": "Change password",
}
db_alias = schema_editor.connection.alias
Flow = apps.get_model("authentik_flows", "Flow")
for flow in Flow.objects.using(db_alias).all():
if flow.slug in slug_title_map:
flow.title = slug_title_map[flow.slug]
else:
flow.title = flow.name
flow.save()
class Migration(migrations.Migration):
@ -45,7 +23,6 @@ class Migration(migrations.Migration):
field=models.TextField(default="", blank=True),
preserve_default=False,
),
migrations.RunPython(add_title_for_defaults),
migrations.AlterField(
model_name="flow",
name="title",

View File

@ -1,27 +1,6 @@
# Generated by Django 3.1.1 on 2020-09-25 23:32
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
# First stage for default-source-enrollment flow (prompt stage)
# needs to have its policy re-evaluated
def update_default_source_enrollment_flow_binding(
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
):
Flow = apps.get_model("authentik_flows", "Flow")
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
db_alias = schema_editor.connection.alias
flows = Flow.objects.using(db_alias).filter(slug="default-source-enrollment")
if not flows.exists():
return
flow = flows.first()
binding = FlowStageBinding.objects.get(target=flow, order=0)
binding.re_evaluate_policies = True
binding.save()
class Migration(migrations.Migration):
@ -47,5 +26,4 @@ class Migration(migrations.Migration):
help_text="When this option is enabled, the planner will re-evaluate policies bound to this binding.",
),
),
migrations.RunPython(update_default_source_enrollment_flow_binding),
]

View File

@ -1,141 +1,12 @@
# Generated by Django 3.1.7 on 2021-04-06 13:25
from django.apps.registry import Apps
from django.contrib.auth.hashers import is_password_usable
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.flows.models import FlowDesignation
PW_USABLE_POLICY_EXPRESSION = """# This policy ensures that the setup flow can only be
# executed when the admin user doesn't have a password set
akadmin = ak_user_by(username="akadmin")
return not akadmin.has_usable_password()"""
PREFILL_POLICY_EXPRESSION = """# This policy sets the user for the currently running flow
# by injecting "pending_user"
akadmin = ak_user_by(username="akadmin")
context["pending_user"] = akadmin
return True"""
def create_default_oobe_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.stages.prompt.models import FieldTypes
User = apps.get_model("authentik_core", "User")
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
Flow = apps.get_model("authentik_flows", "Flow")
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
UserLoginStage = apps.get_model("authentik_stages_user_login", "UserLoginStage")
UserWriteStage = apps.get_model("authentik_stages_user_write", "UserWriteStage")
PromptStage = apps.get_model("authentik_stages_prompt", "PromptStage")
Prompt = apps.get_model("authentik_stages_prompt", "Prompt")
ExpressionPolicy = apps.get_model("authentik_policies_expression", "ExpressionPolicy")
db_alias = schema_editor.connection.alias
# Only create the flow if the akadmin user exists,
# and has an un-usable password
akadmins = User.objects.filter(username="akadmin")
if not akadmins.exists():
return
akadmin = akadmins.first()
if is_password_usable(akadmin.password):
return
# Create a policy that sets the flow's user
prefill_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
name="default-oobe-prefill-user",
defaults={"expression": PREFILL_POLICY_EXPRESSION},
)
password_usable_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
name="default-oobe-password-usable",
defaults={"expression": PW_USABLE_POLICY_EXPRESSION},
)
prompt_header, _ = Prompt.objects.using(db_alias).update_or_create(
field_key="oobe-header-text",
defaults={
"label": "oobe-header-text",
"type": FieldTypes.STATIC,
"placeholder": "Welcome to authentik! Please set a password for the default admin user, akadmin.",
"order": 100,
},
)
prompt_email, _ = Prompt.objects.using(db_alias).update_or_create(
field_key="email",
defaults={
"label": "Email",
"type": FieldTypes.EMAIL,
"placeholder": "Admin email",
"order": 101,
},
)
password_first = Prompt.objects.using(db_alias).get(field_key="password")
password_second = Prompt.objects.using(db_alias).get(field_key="password_repeat")
prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create(
name="default-oobe-password",
)
prompt_stage.fields.set([prompt_header, prompt_email, password_first, password_second])
prompt_stage.save()
user_write, _ = UserWriteStage.objects.using(db_alias).update_or_create(
name="default-password-change-write"
)
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
name="default-authentication-login"
)
flow, _ = Flow.objects.using(db_alias).update_or_create(
slug="initial-setup",
designation=FlowDesignation.STAGE_CONFIGURATION,
defaults={
"name": "default-oobe-setup",
"title": "Welcome to authentik!",
},
)
PolicyBinding.objects.using(db_alias).update_or_create(
policy=password_usable_policy, target=flow, defaults={"order": 0}
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow,
stage=prompt_stage,
defaults={
"order": 10,
},
)
user_write_binding, _ = FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow,
stage=user_write,
defaults={"order": 20, "evaluate_on_plan": False, "re_evaluate_policies": True},
)
PolicyBinding.objects.using(db_alias).update_or_create(
policy=prefill_policy, target=user_write_binding, defaults={"order": 0}
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow,
stage=login_stage,
defaults={
"order": 100,
},
)
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0017_auto_20210329_1334"),
("authentik_stages_user_write", "0002_auto_20200918_1653"),
("authentik_stages_user_login", "0003_session_duration_delta"),
("authentik_stages_password", "0002_passwordstage_change_flow"),
("authentik_policies", "0001_initial"),
("authentik_policies_expression", "0001_initial"),
]
operations = [
migrations.RunPython(create_default_oobe_flow),
]
operations = []

View File

@ -1,21 +1,5 @@
# Generated by Django 4.0 on 2021-12-27 21:03
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def update_title_for_defaults(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
slug_title_map = {
"default-provider-authorization-implicit-consent": "Redirecting to %(app)s",
"default-provider-authorization-explicit-consent": "Redirecting to %(app)s",
}
db_alias = schema_editor.connection.alias
Flow = apps.get_model("authentik_flows", "Flow")
for flow in Flow.objects.using(db_alias).all():
if flow.slug not in slug_title_map:
continue
flow.title = slug_title_map[flow.slug]
flow.save()
from django.db import migrations
class Migration(migrations.Migration):
@ -24,4 +8,4 @@ class Migration(migrations.Migration):
("authentik_flows", "0020_flowtoken"),
]
operations = [migrations.RunPython(update_title_for_defaults)]
operations = []

View File

@ -165,7 +165,7 @@ class Flow(SerializerModel, PolicyBindingModel):
stages = models.ManyToManyField(Stage, through="FlowStageBinding", blank=True)
@property
def serializer(self) -> BaseSerializer:
def serializer(self) -> type[BaseSerializer]:
from authentik.flows.api.flows import FlowSerializer
return FlowSerializer
@ -225,7 +225,7 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
objects = InheritanceManager()
@property
def serializer(self) -> BaseSerializer:
def serializer(self) -> type[BaseSerializer]:
from authentik.flows.api.bindings import FlowStageBindingSerializer
return FlowStageBindingSerializer

View File

@ -1,29 +0,0 @@
"""test example flows in docs"""
from glob import glob
from pathlib import Path
from typing import Callable
from django.test import TransactionTestCase
from authentik.flows.transfer.importer import FlowImporter
class TestTransferDocs(TransactionTestCase):
"""Empty class, test methods are added dynamically"""
def pbflow_tester(file_name: str) -> Callable:
"""This is used instead of subTest for better visibility"""
def tester(self: TestTransferDocs):
with open(file_name, "r", encoding="utf8") as flow_json:
importer = FlowImporter(flow_json.read())
self.assertTrue(importer.validate())
self.assertTrue(importer.apply())
return tester
for flow_file in glob("website/static/flows/*.akflow"):
method_name = Path(flow_file).stem.replace("-", "_").replace(".", "_")
setattr(TestTransferDocs, f"test_flow_{method_name}", pbflow_tester(flow_file))

View File

@ -1,88 +0,0 @@
"""transfer common classes"""
from dataclasses import asdict, dataclass, field, is_dataclass
from enum import Enum
from typing import Any
from uuid import UUID
from django.core.serializers.json import DjangoJSONEncoder
from authentik.lib.models import SerializerModel
from authentik.lib.sentry import SentryIgnoredException
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
"""Get object's attributes via their serializer, and convert it to a normal dict"""
data = dict(obj.serializer(obj).data)
to_remove = (
"policies",
"stages",
"pk",
"background",
"group",
"user",
"verbose_name",
"verbose_name_plural",
"component",
"flow_set",
"promptstage_set",
"policybindingmodel_ptr_id",
"export_url",
"meta_model_name",
)
for to_remove_name in to_remove:
if to_remove_name in data:
data.pop(to_remove_name)
for key in list(data.keys()):
if key.endswith("_obj"):
data.pop(key)
return data
@dataclass
class FlowBundleEntry:
"""Single entry of a bundle"""
identifiers: dict[str, Any]
model: str
attrs: dict[str, Any]
@staticmethod
def from_model(model: SerializerModel, *extra_identifier_names: str) -> "FlowBundleEntry":
"""Convert a SerializerModel instance to a Bundle Entry"""
identifiers = {
"pk": model.pk,
}
all_attrs = get_attrs(model)
for extra_identifier_name in extra_identifier_names:
identifiers[extra_identifier_name] = all_attrs.pop(extra_identifier_name)
return FlowBundleEntry(
identifiers=identifiers,
model=f"{model._meta.app_label}.{model._meta.model_name}",
attrs=all_attrs,
)
@dataclass
class FlowBundle:
"""Dataclass used for a full export"""
version: int = field(default=1)
entries: list[FlowBundleEntry] = field(default_factory=list)
class DataclassEncoder(DjangoJSONEncoder):
"""Convert FlowBundleEntry to json"""
def default(self, o):
if is_dataclass(o):
return asdict(o)
if isinstance(o, UUID):
return str(o)
if isinstance(o, Enum):
return o.value
return super().default(o) # pragma: no cover
class EntryInvalidError(SentryIgnoredException):
"""Error raised when an entry is invalid"""

View File

@ -20,6 +20,17 @@ ENV_PREFIX = "AUTHENTIK"
ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local")
def get_path_from_dict(root: dict, path: str, sep=".", default=None):
"""Recursively walk through `root`, checking each part of `path` split by `sep`.
If at any point a dict does not exist, return default"""
for comp in path.split(sep):
if root and comp in root:
root = root.get(comp)
else:
return default
return root
class ConfigLoader:
"""Search through SEARCH_PATHS and load configuration. Environment variables starting with
`ENV_PREFIX` are also applied.
@ -155,12 +166,7 @@ class ConfigLoader:
# Walk sub_dicts before parsing path
root = self.raw
# Walk each component of the path
for comp in path.split(sep):
if root and comp in root:
root = root.get(comp)
else:
return default
return root
return get_path_from_dict(root, path, sep=sep, default=default)
def y_set(self, path: str, value: Any, sep="."):
"""Set value using same syntax as y()"""

View File

@ -7,11 +7,10 @@ postgresql:
port: 5432
password: 'env://POSTGRES_PASSWORD'
web:
listen: 0.0.0.0:9000
listen_tls: 0.0.0.0:9443
listen:
listen_http: 0.0.0.0:9000
listen_https: 0.0.0.0:9443
listen_metrics: 0.0.0.0:9300
outpost_port_offset: 0
redis:
host: localhost
@ -57,6 +56,7 @@ outposts:
# %(build_hash)s: Build hash if you're running a beta version
container_image_base: ghcr.io/goauthentik/%(type)s:%(version)s
discover: true
disable_embedded_outpost: false
ldap:
tls:
@ -78,3 +78,5 @@ gdpr_compliance: true
cert_discovery_dir: /certs
default_token_length: 128
impersonation: true
blueprints_dir: /blueprints

View File

@ -12,7 +12,7 @@ class SerializerModel(models.Model):
"""Base Abstract Model which has a serializer"""
@property
def serializer(self) -> BaseSerializer:
def serializer(self) -> type[BaseSerializer]:
"""Get serializer for this model"""
raise NotImplementedError

View File

@ -8,7 +8,7 @@ from channels.middleware import BaseMiddleware
from channels_redis.core import ChannelFull
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError
from django.db import InternalError, OperationalError, ProgrammingError
from django.db import DatabaseError, InternalError, OperationalError, ProgrammingError
from django.http.response import Http404
from django_redis.exceptions import ConnectionInterrupted
from docker.errors import DockerException
@ -87,10 +87,6 @@ def sentry_init(**sentry_init_kwargs):
set_tag("authentik.build_hash", get_build_hash("tagged"))
set_tag("authentik.env", get_env())
set_tag("authentik.component", "backend")
LOGGER.info(
"Error reporting is enabled",
env=kwargs["environment"],
)
def traces_sampler(sampling_context: dict) -> float:
@ -116,6 +112,7 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
# Django Errors
Error,
ImproperlyConfigured,
DatabaseError,
OperationalError,
InternalError,
ProgrammingError,

View File

@ -1,8 +0,0 @@
"""authentik lib template utilities"""
from django.template import Context, loader
def render_to_string(template_path: str, ctx: Context) -> str:
"""Render a template to string"""
template = loader.get_template(template_path)
return template.render(ctx)

View File

@ -1,8 +0,0 @@
"""Serializer mixin for managed models"""
from rest_framework.fields import CharField
class ManagedSerializer:
"""Managed Serializer"""
managed = CharField(read_only=True, allow_null=True)

View File

@ -1,10 +0,0 @@
"""authentik Managed app"""
from django.apps import AppConfig
class AuthentikManagedConfig(AppConfig):
"""authentik Managed app"""
name = "authentik.managed"
label = "authentik_managed"
verbose_name = "authentik Managed"

View File

@ -1,70 +0,0 @@
"""Managed objects manager"""
from typing import Callable, Optional
from structlog.stdlib import get_logger
from authentik.managed.models import ManagedModel
LOGGER = get_logger()
class EnsureOp:
"""Ensure operation, executed as part of an ObjectManager run"""
_obj: type[ManagedModel]
_managed_uid: str
_kwargs: dict
def __init__(self, obj: type[ManagedModel], managed_uid: str, **kwargs) -> None:
self._obj = obj
self._managed_uid = managed_uid
self._kwargs = kwargs
def run(self):
"""Do the actual ensure action"""
raise NotImplementedError
class EnsureExists(EnsureOp):
"""Ensure object exists, with kwargs as given values"""
created_callback: Optional[Callable]
def __init__(
self,
obj: type[ManagedModel],
managed_uid: str,
created_callback: Optional[Callable] = None,
**kwargs,
) -> None:
super().__init__(obj, managed_uid, **kwargs)
self.created_callback = created_callback
def run(self):
self._kwargs.setdefault("managed", self._managed_uid)
obj, created = self._obj.objects.update_or_create(
**{
"managed": self._managed_uid,
"defaults": self._kwargs,
}
)
if created and self.created_callback is not None:
self.created_callback(obj)
class ObjectManager:
"""Base class for Apps Object manager"""
def run(self):
"""Main entrypoint for tasks, iterate through all implementation of this
and execute all operations"""
for sub in ObjectManager.__subclasses__():
sub_inst = sub()
ops = sub_inst.reconcile()
LOGGER.debug("Reconciling managed objects", manager=sub.__name__)
for operation in ops:
operation.run()
def reconcile(self) -> list[EnsureOp]:
"""Method which is implemented in subclass that returns a list of Operations"""
raise NotImplementedError

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