Compare commits

..

107 Commits

Author SHA1 Message Date
e0f48a30b7 release: 2021.6.1-rc6 2021-06-15 21:18:33 +02:00
973f14d911 ci: only build stable images when non-rc version
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-15 18:41:26 +02:00
e8978adc1b outpost: fix syntax error when creating an outpost with connection
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-15 18:39:51 +02:00
3ca8d9c968 ci: build and push stable tag when rc not in release name
closes #1023

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-15 17:34:23 +02:00
42636142fa build(deps): bump @typescript-eslint/parser in /web (#1021)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.26.1 to 4.27.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.27.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>
2021-06-15 09:29:14 +02:00
57c459348f build(deps): bump @sentry/tracing from 6.6.0 to 6.7.0 in /web (#1020)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 6.6.0 to 6.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/6.6.0...6.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>
2021-06-15 09:28:55 +02:00
493b34cf0d build(deps): bump boto3 from 1.17.93 to 1.17.94 (#1022) 2021-06-15 08:55:32 +02:00
f0493f418b build(deps): bump @sentry/browser from 6.6.0 to 6.7.0 in /web (#1019) 2021-06-15 08:55:05 +02:00
d45a292652 build(deps): bump @babel/core from 7.14.5 to 7.14.6 in /web (#1018) 2021-06-15 08:54:44 +02:00
b21ea360db build(deps): bump @lingui/core from 3.10.2 to 3.10.3 in /web (#1016) 2021-06-15 08:54:36 +02:00
6816f8b851 build(deps): bump postcss from 8.3.2 to 8.3.4 in /website (#1015) 2021-06-15 08:54:18 +02:00
de714f0390 build(deps): bump @typescript-eslint/eslint-plugin in /web (#1017) 2021-06-15 08:54:10 +02:00
800df332b5 stages/authenticator_duo: don't create default duo stage
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-14 22:55:37 +02:00
16c194d2dc core: fix upload api not checking clear properly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-14 22:34:47 +02:00
53100a72fe stages/identification: fix challenges not being annotated correctly and API client not loading data correctly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-14 22:28:11 +02:00
ec4c3f44cb events: don't create system exception event in debug
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-14 22:16:27 +02:00
f10bd432b3 policies/reputation: fix race condition in tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-14 20:40:40 +02:00
4de927ba5b web/admin: fix link for github issue creation
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-14 18:55:43 +02:00
74e578c2bf events: add tenant to event
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-14 18:43:29 +02:00
e584fd1344 events: catch unhandled exceptions from request as event, add button to open github issue
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-14 17:22:58 +02:00
0e02925a3d stages/authenticator_validate: add tests for authenticator validation
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-14 16:32:36 +02:00
5b837c3ccc providers/saml: improve error handling for signature errors
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-14 12:51:42 +02:00
2580371f94 outposts: fix error when getting component for base service connection
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-14 12:38:29 +02:00
4e9be85353 website/docs: add docs for outpost configuration
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-14 09:21:35 +02:00
79508e1965 core: fix linting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 23:41:50 +02:00
3a88dde545 web: fix declaration of Intl
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 23:13:43 +02:00
31fc4d1cb9 web: migrate banner to sidebar
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 22:55:41 +02:00
09cd8f8f63 web/admin: fix ak-application-check-access-form for get api
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 22:40:51 +02:00
d824b09365 outposts/ldap: improve responses for unsuccessful binds
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 22:00:05 +02:00
cabbd18880 core: revert check_access API to get to prevent CSRF errors
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 21:47:49 +02:00
c9dda17c68 web/admin: select service connection by default when only one exists
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 20:12:01 +02:00
bb8559ee18 web: remove base interface
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 19:54:27 +02:00
5ae32e525c web/flows: improve display of allowed fields for identification stage
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 16:30:03 +02:00
0832145a01 web: fix fields for new api schema
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 15:36:25 +02:00
4167276c8f root: fix references to helm chart
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 14:30:44 +02:00
afb84c7bc5 flows: fix error clearing flow background when no files have been uploaded
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 14:14:41 +02:00
82b2c7e3f0 web: add capabilities to sentry event
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 14:08:39 +02:00
fc8004db2b outposts: fix integrity error with tokens
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 13:36:54 +02:00
ddfc943bba root: fix build_hash being set incorrectly for tagged versions
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 13:32:18 +02:00
8c0c12292e build(deps): bump tslib from 2.2.0 to 2.3.0 in /web (#1011)
Bumps [tslib](https://github.com/Microsoft/tslib) from 2.2.0 to 2.3.0.
- [Release notes](https://github.com/Microsoft/tslib/releases)
- [Commits](https://github.com/Microsoft/tslib/compare/2.2.0...2.3.0)

---
updated-dependencies:
- dependency-name: tslib
  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>
2021-06-13 13:08:50 +02:00
803490d98b build(deps): bump rollup from 2.51.1 to 2.51.2 in /web (#1012)
Bumps [rollup](https://github.com/rollup/rollup) from 2.51.1 to 2.51.2.
- [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.51.1...v2.51.2)

---
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>
2021-06-13 13:08:38 +02:00
16835ab478 build(deps): bump boto3 from 1.17.92 to 1.17.93 (#1013)
Bumps [boto3](https://github.com/boto/boto3) from 1.17.92 to 1.17.93.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.17.92...1.17.93)

---
updated-dependencies:
- dependency-name: boto3
  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>
2021-06-13 13:08:27 +02:00
572b8d87b5 api: fix import error
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 12:59:28 +02:00
31d2ea65fd provider/proxy: mark forward_auth flag as deprecated
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 12:39:25 +02:00
f4ac2f50e2 sources/saml: check sessions before deleting user
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 12:39:10 +02:00
969a3f0ddd build(deps): bump drf-spectacular from 0.17.0 to 0.17.1 (#1014)
Bumps [drf-spectacular](https://github.com/tfranzel/drf-spectacular) from 0.17.0 to 0.17.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.17.0...0.17.1)

---
updated-dependencies:
- dependency-name: drf-spectacular
  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>
2021-06-13 12:28:24 +02:00
4e18f47f28 web/flows: fix expiry not shown on consent stage when loading
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-13 12:21:11 +02:00
f10286edf8 Merge branch 'version-2021.6' into next 2021-06-12 20:43:12 +02:00
d789dcc28f core: fix impersonation not working with inactive users
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-12 20:41:02 +02:00
715a71427e web/admin: fix user enable/disable modal not matching other modals
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-12 20:31:02 +02:00
84c21d16cf website: fix duplicate plugin ID
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-12 20:15:35 +02:00
2e4e17adb7 web/flows: fix IdentificationStage's label not matching fields
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-12 18:49:50 +02:00
00cbaaf672 web/flows: improve display of errors
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-12 18:18:36 +02:00
74e4e8f6aa core: delete real session when AuthenticatedSession is deleted
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-12 17:37:32 +02:00
d78fda990a release: 2021.6.1-rc5 2021-06-12 15:19:24 +02:00
10d949f7a9 stages/password: add constants for password backends
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-12 12:14:55 +02:00
6661af032d build(deps): bump @sentry/tracing from 6.5.1 to 6.6.0 in /web (#1007)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 6.5.1 to 6.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/6.5.1...6.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>
2021-06-11 09:28:58 +02:00
fb5e4a3af8 build(deps): bump postcss from 8.3.1 to 8.3.2 in /website (#1006) 2021-06-11 08:25:06 +02:00
1dfad83a34 build(deps): bump @sentry/browser from 6.5.1 to 6.6.0 in /web (#1008) 2021-06-11 08:24:48 +02:00
70025c648c build(deps): bump boto3 from 1.17.91 to 1.17.92 (#1009) 2021-06-11 08:24:24 +02:00
676b77aa7c stages/identification: add UPN
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 22:48:39 +02:00
e35e096266 stages/authenticator_webauthn: use tenant title as RP_NAME
closes #1004

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 22:17:25 +02:00
7af12d4fec stages/authenticator_totp: set TOTP issuer based on slug'd tenant title
closes #1004

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 22:16:37 +02:00
8d6db0fabf flows: fix configuration URL being set when no flow is configure
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 22:07:26 +02:00
8ddcf99bf7 web: fix flow download link
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 21:47:40 +02:00
e25f6aea8c release: 2021.6.1-rc4 2021-06-10 18:59:00 +02:00
b1a9eda1d3 ci: fix release test using wrong docker image
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 18:58:30 +02:00
2c15ab9995 release: 2021.6.1-rc3 2021-06-10 18:04:59 +02:00
b3c51e426d web: fix styling for toggle group on dark mode
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 18:02:27 +02:00
71578af47f ci: fix testing for release tag
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 17:41:54 +02:00
6c985acb36 release: 2021.6.1-rc2 2021-06-10 14:10:47 +02:00
d878d2140e providers/saml: add metadata download link to api
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 14:06:44 +02:00
4766d6ff3d flows: add export URL to API
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 13:52:50 +02:00
3a64d97040 crypto: add download links as API fields
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 13:46:12 +02:00
2275ba3add flows: fix get_pending_user returning in-memory user when PLAN_CONTEXT_PENDING_USER_IDENTIFIER is set
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 12:17:46 +02:00
9f7c941426 Merge branch 'master' into next 2021-06-10 11:59:10 +02:00
34ae9e6dab API: add endpoint to show by what objects an object is used (#995)
* core: add used_by API to show what objects are affected before deletion

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

* web/elements: add support for used_by API

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

* core: add authentik_used_by_shadows to shadow other models

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

* web: implement used_by API

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

* *: fix duplicate imports

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

* core: add action field to used_by api

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

* web: add UI for used_by action

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

* web: add notice to tenant form

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

* core: fix naming in used_by

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

* web: check length for used_by

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

* core: fix used_by for non-pk models

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

* *: improve __str__ on models

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

* core: add support for many to many in used_by

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 11:58:12 +02:00
bf683514ee build(deps): bump @babel/plugin-proposal-decorators in /web (#1000) 2021-06-10 09:11:01 +02:00
9b58bdb447 build(deps): bump @babel/preset-env from 7.14.4 to 7.14.5 in /web (#1002) 2021-06-10 09:10:52 +02:00
4237f20ccd build(deps): bump boto3 from 1.17.90 to 1.17.91 (#1003) 2021-06-10 08:53:42 +02:00
2408719a47 build(deps): bump eslint-plugin-lit from 1.5.0 to 1.5.1 in /web (#1001) 2021-06-10 08:53:35 +02:00
b33fef7929 build(deps): bump @babel/preset-typescript from 7.13.0 to 7.14.5 in /web (#999) 2021-06-10 08:53:20 +02:00
73b9847e7d build(deps): bump @babel/core from 7.14.3 to 7.14.5 in /web (#998) 2021-06-10 08:53:10 +02:00
a7e4eb021d build(deps): bump @babel/plugin-transform-runtime in /web (#997) 2021-06-10 08:53:01 +02:00
11306770ad build(deps): bump postcss from 8.3.0 to 8.3.1 in /website (#996) 2021-06-10 08:52:51 +02:00
5235e00d3c stages/authenticator_validate: add more logging for challenges
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 23:58:08 +02:00
7834146efc web/admin: fix authenticatior_valiation stage not setting correct fields
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 19:38:54 +02:00
d4379ecd31 flows: fix configure_url not being set correctly User settings
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 19:25:27 +02:00
7492608ace Merge branch 'version-2021.6' into next 2021-06-09 16:06:06 +02:00
7eef501446 Revert "root: fix permissions for docker files"
This reverts commit a7adeb917e.
2021-06-09 16:04:17 +02:00
b73de96aa6 lifecycle: fix permissions for unittest xml
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 16:03:51 +02:00
a7adeb917e root: fix permissions for docker files
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 16:00:29 +02:00
4ee2f951da lifecycle: fix check_if_root not working without docker
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:56:12 +02:00
01c5235e82 ci: use bootstrap for testing
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:54:47 +02:00
0ce4f9fe12 Revert "web: don't build api client as separate bundle"
This reverts commit 7c1fe1243f.
2021-06-09 15:37:57 +02:00
2f4f951818 Revert "web: build API during npm build"
This reverts commit a6c214e8fa.
2021-06-09 15:37:50 +02:00
a6c214e8fa web: build API during npm build
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:35:35 +02:00
57f8b108c4 root: remove production=false
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:27:06 +02:00
7c1fe1243f web: don't build api client as separate bundle
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:26:42 +02:00
3f69dd34ba ci: run tests as authentik
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:05:03 +02:00
c81431895a Merge branch 'master' into version-2021.6 2021-06-09 15:04:52 +02:00
560c979d26 root: fix requirements-dev including all dependencies
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 14:22:45 +02:00
c5cc8842ec root: fix missing test files in docker file
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 14:22:32 +02:00
2a881d241d Merge branch 'master' into next 2021-06-09 11:25:07 +02:00
6291834573 outpost: fix missing outpost images
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 11:24:59 +02:00
eeea36acea outpost: fix missing outpost images
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 11:22:28 +02:00
e95b9da586 website/docs: fix beta instructions for k8s
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 11:07:02 +02:00
205 changed files with 5677 additions and 2200 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2021.6.1-rc1 current_version = 2021.6.1-rc6
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)

View File

@ -1,5 +1,4 @@
env env
helm
static static
htmlcov htmlcov
*.env.yml *.env.yml

View File

@ -33,12 +33,22 @@ jobs:
with: with:
push: ${{ github.event_name == 'release' }} push: ${{ github.event_name == 'release' }}
tags: | tags: |
beryju/authentik:2021.6.1-rc1, beryju/authentik:2021.6.1-rc6,
beryju/authentik:latest, beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.6.1-rc1, ghcr.io/goauthentik/server:2021.6.1-rc6,
ghcr.io/goauthentik/server:latest ghcr.io/goauthentik/server:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
context: . context: .
- name: Building Docker Image (stable)
uses: docker/build-push-action@v2
if: ${{ github.event_name == 'release' && !contains('2021.6.1-rc6', 'rc') }}
with:
push: true
tags: |
beryju/authentik:stable,
ghcr.io/goauthentik/server:stable
platforms: linux/amd64,linux/arm64
context: .
build-proxy: build-proxy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -66,12 +76,22 @@ jobs:
with: with:
push: ${{ github.event_name == 'release' }} push: ${{ github.event_name == 'release' }}
tags: | tags: |
beryju/authentik-proxy:2021.6.1-rc1, beryju/authentik-proxy:2021.6.1-rc6,
beryju/authentik-proxy:latest, beryju/authentik-proxy:latest,
ghcr.io/goauthentik/proxy:2021.6.1-rc1, ghcr.io/goauthentik/proxy:2021.6.1-rc6,
ghcr.io/goauthentik/proxy:latest ghcr.io/goauthentik/proxy:latest
file: outpost/proxy.Dockerfile file: outpost/proxy.Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable)
uses: docker/build-push-action@v2
if: ${{ github.event_name == 'release' && !contains('2021.6.1-rc6', 'rc') }}
with:
push: true
tags: |
beryju/authentik-proxy:stable,
ghcr.io/goauthentik/proxy:stable
platforms: linux/amd64,linux/arm64
context: .
build-ldap: build-ldap:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -99,12 +119,22 @@ jobs:
with: with:
push: ${{ github.event_name == 'release' }} push: ${{ github.event_name == 'release' }}
tags: | tags: |
beryju/authentik-ldap:2021.6.1-rc1, beryju/authentik-ldap:2021.6.1-rc6,
beryju/authentik-ldap:latest, beryju/authentik-ldap:latest,
ghcr.io/goauthentik/ldap:2021.6.1-rc1, ghcr.io/goauthentik/ldap:2021.6.1-rc6,
ghcr.io/goauthentik/ldap:latest ghcr.io/goauthentik/ldap:latest
file: outpost/ldap.Dockerfile file: outpost/ldap.Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable)
uses: docker/build-push-action@v2
if: ${{ github.event_name == 'release' && !contains('2021.6.1-rc6', 'rc') }}
with:
push: true
tags: |
beryju/authentik-ldap:stable,
ghcr.io/goauthentik/ldap:stable
platforms: linux/amd64,linux/arm64
context: .
test-release: test-release:
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}
needs: needs:
@ -122,7 +152,7 @@ jobs:
docker-compose pull -q docker-compose pull -q
docker-compose up --no-start docker-compose up --no-start
docker-compose start postgresql redis docker-compose start postgresql redis
docker-compose run -u root --entrypoint /bin/bash server -c "apt-get update && apt-get install -y --no-install-recommends git && pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik" docker-compose run -u root server test
sentry-release: sentry-release:
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}
needs: needs:
@ -138,5 +168,5 @@ jobs:
SENTRY_PROJECT: authentik SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org SENTRY_URL: https://sentry.beryju.org
with: with:
version: authentik@2021.6.1-rc1 version: authentik@2021.6.1-rc6
environment: beryjuorg-prod environment: beryjuorg-prod

View File

@ -20,11 +20,11 @@ jobs:
docker-compose pull -q docker-compose pull -q
docker build \ docker build \
--no-cache \ --no-cache \
-t beryju/authentik:latest \ -t ghcr.io/goauthentik/server:latest \
-f Dockerfile . -f Dockerfile .
docker-compose up --no-start docker-compose up --no-start
docker-compose start postgresql redis docker-compose start postgresql redis
docker-compose run -u root --entrypoint /bin/bash server -c "apt-get update && apt-get install -y --no-install-recommends git && pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik" docker-compose run -u root server test
- name: Extract version number - name: Extract version number
id: get_version id: get_version
uses: actions/github-script@v4.0.2 uses: actions/github-script@v4.0.2

4
.gitignore vendored
View File

@ -193,10 +193,6 @@ pip-selfcheck.json
local.env.yml local.env.yml
.vscode/ .vscode/
### Helm ###
# Chart dependencies
**/charts/*.tgz
# Selenium Screenshots # Selenium Screenshots
selenium_screenshots/ selenium_screenshots/
backups/ backups/

View File

@ -8,7 +8,7 @@ WORKDIR /app/
RUN pip install pipenv && \ RUN pip install pipenv && \
pipenv lock -r > requirements.txt && \ pipenv lock -r > requirements.txt && \
pipenv lock -rd > requirements-dev.txt pipenv lock -r --dev-only > requirements-dev.txt
# Stage 2: Build web API # Stage 2: Build web API
FROM openapitools/openapi-generator-cli as api-builder FROM openapitools/openapi-generator-cli as api-builder
@ -28,7 +28,7 @@ COPY ./web /static/
COPY --from=api-builder /local/web/api /static/api COPY --from=api-builder /local/web/api /static/api
ENV NODE_ENV=production ENV NODE_ENV=production
RUN cd /static && npm i --production=false && npm run build RUN cd /static && npm i && npm run build
# Stage 4: Build go proxy # Stage 4: Build go proxy
FROM golang:1.16.5 AS builder FROM golang:1.16.5 AS builder
@ -76,6 +76,7 @@ RUN apt-get update && \
COPY ./authentik/ /authentik COPY ./authentik/ /authentik
COPY ./pyproject.toml / COPY ./pyproject.toml /
COPY ./xml /xml COPY ./xml /xml
COPY ./tests /tests
COPY ./manage.py / COPY ./manage.py /
COPY ./lifecycle/ /lifecycle COPY ./lifecycle/ /lifecycle
COPY --from=builder /work/authentik /authentik-proxy COPY --from=builder /work/authentik /authentik-proxy

118
Pipfile.lock generated
View File

@ -56,6 +56,7 @@
"sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a",
"sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"
], ],
"markers": "python_version >= '3.6'",
"version": "==3.7.4.post0" "version": "==3.7.4.post0"
}, },
"aioredis": { "aioredis": {
@ -70,6 +71,7 @@
"sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2", "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2",
"sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb" "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"
], ],
"markers": "python_version >= '3.6'",
"version": "==5.0.6" "version": "==5.0.6"
}, },
"asgiref": { "asgiref": {
@ -77,6 +79,7 @@
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee", "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78" "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
], ],
"markers": "python_version >= '3.6'",
"version": "==3.3.4" "version": "==3.3.4"
}, },
"async-timeout": { "async-timeout": {
@ -84,6 +87,7 @@
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
], ],
"markers": "python_full_version >= '3.5.3'",
"version": "==3.0.1" "version": "==3.0.1"
}, },
"attrs": { "attrs": {
@ -91,6 +95,7 @@
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.2.0" "version": "==21.2.0"
}, },
"autobahn": { "autobahn": {
@ -98,6 +103,7 @@
"sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac", "sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac",
"sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03" "sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03"
], ],
"markers": "python_version >= '3.7'",
"version": "==21.3.1" "version": "==21.3.1"
}, },
"automat": { "automat": {
@ -116,24 +122,26 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:2ade860f66fa6b9a9886d7ff2e5118e5efebc4807b863ef735d358ef730234ed", "sha256:6180272094030bda3ee5c242881892cd3d9d19c05cb513945f530e396c7de1e4",
"sha256:bbf727d770a9844834bfbf3f811db1d3438320897f67cfb21cdca5bb8fc23c13" "sha256:95d814d16fe55ae55e1e4a3db248596f9647a0c42f4796c6e05be0bfaffb1830"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.17.90" "version": "==1.17.94"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:6ae4ff3405cc4fc69ff3673a8dd234bf869aa556ae1e0da050d7f2aa3c3edab6", "sha256:60a382a5b2f7d77b1b575d54fba819097526e3fdd0f3004f4d1142d50af0d642",
"sha256:b301810c4bd6cab1b6eaf6bfd9f25abb27959b586c2e1689bbce035b3fb8ae66" "sha256:ba8a7951be535e25219a82dea15c30d7bdf0c51e7c1623c3306248493c1616ac"
], ],
"version": "==1.20.90" "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.20.94"
}, },
"cachetools": { "cachetools": {
"hashes": [ "hashes": [
"sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001", "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001",
"sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff" "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"
], ],
"markers": "python_version ~= '3.5'",
"version": "==4.2.2" "version": "==4.2.2"
}, },
"cbor2": { "cbor2": {
@ -152,6 +160,7 @@
"sha256:ce6219986385778b1ab7f9b542f160bb4d3558f52975e914a27b774e47016fb7", "sha256:ce6219986385778b1ab7f9b542f160bb4d3558f52975e914a27b774e47016fb7",
"sha256:d562b2773e14ee1d65ea5b85351a83a64d4f3fd011bc2b4c70a6e813e78203ce" "sha256:d562b2773e14ee1d65ea5b85351a83a64d4f3fd011bc2b4c70a6e813e78203ce"
], ],
"markers": "python_version >= '3.6'",
"version": "==5.4.0" "version": "==5.4.0"
}, },
"celery": { "celery": {
@ -244,6 +253,7 @@
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.0.0" "version": "==4.0.0"
}, },
"click": { "click": {
@ -251,6 +261,7 @@
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2" "version": "==7.1.2"
}, },
"click-didyoumean": { "click-didyoumean": {
@ -310,6 +321,7 @@
"sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f", "sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f",
"sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393" "sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"
], ],
"markers": "python_version >= '3.6'",
"version": "==3.0.2" "version": "==3.0.2"
}, },
"defusedxml": { "defusedxml": {
@ -414,11 +426,11 @@
}, },
"drf-spectacular": { "drf-spectacular": {
"hashes": [ "hashes": [
"sha256:4d35e890b8139e1c056588c5529a2f2066615635482563f0840b96d3b879d7d2", "sha256:146e8c21dc806a20c84c687811c30163970fbf620213ab87280f7403469d80bb",
"sha256:f552476dfde647963c21615249672e7f4f9ece3788036b5ee5c6cc5ad50748ab" "sha256:8a028d251a6d0b39739ebdec487fd43ee4ecba244d8ffaaac43ff06430728dd8"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.17.0" "version": "==0.17.1"
}, },
"duo-client": { "duo-client": {
"hashes": [ "hashes": [
@ -440,6 +452,7 @@
"hashes": [ "hashes": [
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.18.2" "version": "==0.18.2"
}, },
"geoip2": { "geoip2": {
@ -452,10 +465,11 @@
}, },
"google-auth": { "google-auth": {
"hashes": [ "hashes": [
"sha256:9b235dbc876e49454cbedc52ae0abd540ef705ebccdf4fbe93553bb13f26b1a4", "sha256:154f7889c5d679a6f626f36adb12afbd4dbb0a9a04ec575d989d6ba79c4fd65e",
"sha256:eb017521276a75492282c6ca4b718f26de112ed3bcbeaeeb02c1b82de425f909" "sha256:6d47c79b5d09fbc7e8355fd9594cc4cf65fdde5d401c63951eaac4baa1ba2ae1"
], ],
"version": "==1.30.2" "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.31.0"
}, },
"gunicorn": { "gunicorn": {
"hashes": [ "hashes": [
@ -470,6 +484,7 @@
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
], ],
"markers": "python_version >= '3.6'",
"version": "==0.12.0" "version": "==0.12.0"
}, },
"hiredis": { "hiredis": {
@ -516,6 +531,7 @@
"sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0", "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0",
"sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a" "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"
], ],
"markers": "python_version >= '3.6'",
"version": "==2.0.0" "version": "==2.0.0"
}, },
"httptools": { "httptools": {
@ -564,6 +580,7 @@
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2" "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
], ],
"markers": "python_version >= '3.5'",
"version": "==0.5.1" "version": "==0.5.1"
}, },
"jmespath": { "jmespath": {
@ -571,6 +588,7 @@
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.0" "version": "==0.10.0"
}, },
"jsonschema": { "jsonschema": {
@ -585,6 +603,7 @@
"sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d", "sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d",
"sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a" "sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a"
], ],
"markers": "python_version >= '3.6'",
"version": "==5.1.0" "version": "==5.1.0"
}, },
"kubernetes": { "kubernetes": {
@ -598,6 +617,9 @@
"ldap3": { "ldap3": {
"hashes": [ "hashes": [
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91", "sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91",
"sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59",
"sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c",
"sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056",
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57" "sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57"
], ],
"index": "pypi", "index": "pypi",
@ -659,6 +681,7 @@
"hashes": [ "hashes": [
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc" "sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
], ],
"markers": "python_version >= '3.6'",
"version": "==2.0.3" "version": "==2.0.3"
}, },
"msgpack": { "msgpack": {
@ -734,6 +757,7 @@
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
], ],
"markers": "python_version >= '3.6'",
"version": "==5.1.0" "version": "==5.1.0"
}, },
"oauthlib": { "oauthlib": {
@ -741,6 +765,7 @@
"sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc", "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc",
"sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3" "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"
], ],
"markers": "python_version >= '3.6'",
"version": "==3.1.1" "version": "==3.1.1"
}, },
"packaging": { "packaging": {
@ -756,6 +781,7 @@
"sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86", "sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86",
"sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2" "sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.11.0" "version": "==0.11.0"
}, },
"prompt-toolkit": { "prompt-toolkit": {
@ -763,6 +789,7 @@
"sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04", "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04",
"sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc" "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc"
], ],
"markers": "python_full_version >= '3.6.1'",
"version": "==3.0.18" "version": "==3.0.18"
}, },
"psycopg2-binary": { "psycopg2-binary": {
@ -808,15 +835,37 @@
}, },
"pyasn1": { "pyasn1": {
"hashes": [ "hashes": [
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
], ],
"version": "==0.4.8" "version": "==0.4.8"
}, },
"pyasn1-modules": { "pyasn1-modules": {
"hashes": [ "hashes": [
"sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8",
"sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",
"sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",
"sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",
"sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4",
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",
"sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",
"sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
"sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd",
"sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",
"sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",
"sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"
], ],
"version": "==0.2.8" "version": "==0.2.8"
}, },
@ -825,6 +874,7 @@
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.20" "version": "==2.20"
}, },
"pycryptodome": { "pycryptodome": {
@ -868,6 +918,7 @@
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
], ],
"markers": "python_version >= '3.5'",
"version": "==2.0.2" "version": "==2.0.2"
}, },
"pyjwt": { "pyjwt": {
@ -890,12 +941,14 @@
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7" "version": "==2.4.7"
}, },
"pyrsistent": { "pyrsistent": {
"hashes": [ "hashes": [
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e" "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
], ],
"markers": "python_version >= '3.5'",
"version": "==0.17.3" "version": "==0.17.3"
}, },
"python-dateutil": { "python-dateutil": {
@ -903,6 +956,7 @@
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.1" "version": "==2.8.1"
}, },
"python-dotenv": { "python-dotenv": {
@ -959,6 +1013,7 @@
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==3.5.3" "version": "==3.5.3"
}, },
"requests": { "requests": {
@ -966,12 +1021,14 @@
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.25.1" "version": "==2.25.1"
}, },
"requests-oauthlib": { "requests-oauthlib": {
"hashes": [ "hashes": [
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.3.0" "version": "==1.3.0"
@ -1012,6 +1069,7 @@
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0" "version": "==1.16.0"
}, },
"sqlparse": { "sqlparse": {
@ -1019,6 +1077,7 @@
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
], ],
"markers": "python_version >= '3.5'",
"version": "==0.4.1" "version": "==0.4.1"
}, },
"structlog": { "structlog": {
@ -1074,6 +1133,7 @@
"sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8", "sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8",
"sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb" "sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb"
], ],
"markers": "python_version >= '3.6'",
"version": "==21.2.1" "version": "==21.2.1"
}, },
"typing-extensions": { "typing-extensions": {
@ -1097,6 +1157,7 @@
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.0.1" "version": "==3.0.1"
}, },
"urllib3": { "urllib3": {
@ -1141,6 +1202,7 @@
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30", "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e" "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
], ],
"markers": "python_version >= '3.6'",
"version": "==5.0.0" "version": "==5.0.0"
}, },
"watchgod": { "watchgod": {
@ -1167,10 +1229,11 @@
}, },
"websocket-client": { "websocket-client": {
"hashes": [ "hashes": [
"sha256:3e2bf58191d4619b161389a95bdce84ce9e0b24eb8107e7e590db682c2d0ca81", "sha256:b68e4959d704768fa20e35c9d508c8dc2bbc041fd8d267c0d7345cffe2824568",
"sha256:abf306dc6351dcef07f4d40453037e51cc5d9da2ef60d0fc5d0fe3bcda255372" "sha256:e5c333bfa9fa739538b652b6f8c8fc2559f1d364243c8a689d7c0e1d41c2e611"
], ],
"version": "==1.0.1" "markers": "python_version >= '3.6'",
"version": "==1.1.0"
}, },
"websockets": { "websockets": {
"hashes": [ "hashes": [
@ -1267,6 +1330,7 @@
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
], ],
"markers": "python_version >= '3.6'",
"version": "==1.6.3" "version": "==1.6.3"
}, },
"zope.interface": { "zope.interface": {
@ -1323,6 +1387,7 @@
"sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4",
"sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==5.4.0" "version": "==5.4.0"
} }
}, },
@ -1339,6 +1404,7 @@
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e", "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975" "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
], ],
"markers": "python_version ~= '3.6'",
"version": "==2.5.6" "version": "==2.5.6"
}, },
"attrs": { "attrs": {
@ -1346,6 +1412,7 @@
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.2.0" "version": "==21.2.0"
}, },
"bandit": { "bandit": {
@ -1384,6 +1451,7 @@
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.0.0" "version": "==4.0.0"
}, },
"click": { "click": {
@ -1391,6 +1459,7 @@
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2" "version": "==7.1.2"
}, },
"colorama": { "colorama": {
@ -1464,6 +1533,7 @@
"sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0",
"sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"
], ],
"markers": "python_version >= '3.4'",
"version": "==4.0.7" "version": "==4.0.7"
}, },
"gitpython": { "gitpython": {
@ -1471,6 +1541,7 @@
"sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135", "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135",
"sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e" "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"
], ],
"markers": "python_version >= '3.5'",
"version": "==3.1.17" "version": "==3.1.17"
}, },
"idna": { "idna": {
@ -1492,6 +1563,7 @@
"sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6", "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6",
"sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d" "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"
], ],
"markers": "python_version >= '3.6' and python_version < '4'",
"version": "==5.8.0" "version": "==5.8.0"
}, },
"lazy-object-proxy": { "lazy-object-proxy": {
@ -1519,6 +1591,7 @@
"sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93",
"sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.6.0" "version": "==1.6.0"
}, },
"mccabe": { "mccabe": {
@ -1555,6 +1628,7 @@
"sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd", "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd",
"sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4" "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"
], ],
"markers": "python_version >= '2.6'",
"version": "==5.6.0" "version": "==5.6.0"
}, },
"pluggy": { "pluggy": {
@ -1562,6 +1636,7 @@
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.13.1" "version": "==0.13.1"
}, },
"py": { "py": {
@ -1569,6 +1644,7 @@
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.10.0" "version": "==1.10.0"
}, },
"pylint": { "pylint": {
@ -1599,6 +1675,7 @@
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7" "version": "==2.4.7"
}, },
"pytest": { "pytest": {
@ -1703,6 +1780,7 @@
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.25.1" "version": "==2.25.1"
}, },
"requests-mock": { "requests-mock": {
@ -1726,6 +1804,7 @@
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0" "version": "==1.16.0"
}, },
"smmap": { "smmap": {
@ -1733,6 +1812,7 @@
"sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182", "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182",
"sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2" "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"
], ],
"markers": "python_version >= '3.5'",
"version": "==4.0.0" "version": "==4.0.0"
}, },
"stevedore": { "stevedore": {
@ -1740,6 +1820,7 @@
"sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee", "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee",
"sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a" "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"
], ],
"markers": "python_version >= '3.6'",
"version": "==3.3.0" "version": "==3.3.0"
}, },
"toml": { "toml": {
@ -1747,6 +1828,7 @@
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2" "version": "==0.10.2"
}, },
"urllib3": { "urllib3": {

View File

@ -21,7 +21,7 @@ authentik is an open-source Identity Provider focused on flexibility and versati
For small/test setups it is recommended to use docker-compose, see the [documentation](https://goauthentik.io/docs/installation/docker-compose/) For small/test setups it is recommended to use docker-compose, see the [documentation](https://goauthentik.io/docs/installation/docker-compose/)
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://goauthentik.io/docs/installation/kubernetes/) For bigger setups, there is a Helm Chart [here])(https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/)
## Screenshots ## Screenshots

View File

@ -1,3 +1,3 @@
"""authentik""" """authentik"""
__version__ = "2021.6.1-rc1" __version__ = "2021.6.1-rc6"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -10,3 +10,25 @@ class AuthentikAPIConfig(AppConfig):
label = "authentik_api" label = "authentik_api"
mountpoint = "api/" mountpoint = "api/"
verbose_name = "authentik API" verbose_name = "authentik API"
def ready(self) -> None:
from drf_spectacular.extensions import OpenApiAuthenticationExtension
from authentik.api.authentication import TokenAuthentication
# Class is defined here as it needs to be created early enough that drf-spectacular will
# find it, but also won't cause any import issues
# pylint: disable=unused-variable
class TokenSchema(OpenApiAuthenticationExtension):
"""Auth schema"""
target_class = TokenAuthentication
name = "authentik"
def get_security_definition(self, auto_schema):
"""Auth schema"""
return {
"type": "apiKey",
"in": "header",
"name": "Authorization",
}

View File

@ -3,7 +3,6 @@ from base64 import b64decode
from binascii import Error from binascii import Error
from typing import Any, Optional, Union from typing import Any, Optional, Union
from drf_spectacular.authentication import OpenApiAuthenticationExtension
from rest_framework.authentication import BaseAuthentication, get_authorization_header from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework.exceptions import AuthenticationFailed from rest_framework.exceptions import AuthenticationFailed
from rest_framework.request import Request from rest_framework.request import Request
@ -56,18 +55,3 @@ class TokenAuthentication(BaseAuthentication):
return None return None
return (token.user, None) # pragma: no cover return (token.user, None) # pragma: no cover
class TokenSchema(OpenApiAuthenticationExtension):
"""Auth schema"""
target_class = TokenAuthentication
name = "authentik"
def get_security_definition(self, auto_schema):
"""Auth schema"""
return {
"type": "apiKey",
"in": "header",
"name": "Authorization",
}

View File

@ -11,13 +11,7 @@ from drf_spectacular.utils import (
inline_serializer, inline_serializer,
) )
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import ( from rest_framework.fields import BooleanField, CharField, FileField, ReadOnlyField
BooleanField,
CharField,
FileField,
IntegerField,
ReadOnlyField,
)
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
@ -29,6 +23,7 @@ from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import Application, User from authentik.core.models import Application, User
from authentik.events.models import EventAction from authentik.events.models import EventAction
from authentik.policies.api.exec import PolicyTestResultSerializer from authentik.policies.api.exec import PolicyTestResultSerializer
@ -73,7 +68,7 @@ class ApplicationSerializer(ModelSerializer):
} }
class ApplicationViewSet(ModelViewSet): class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Application Viewset""" """Application Viewset"""
queryset = Application.objects.all() queryset = Application.objects.all()
@ -106,15 +101,19 @@ class ApplicationViewSet(ModelViewSet):
return applications return applications
@extend_schema( @extend_schema(
request=inline_serializer( parameters=[
"CheckAccessRequest", fields={"for_user": IntegerField(required=False)} OpenApiParameter(
), name="for_user",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.INT,
)
],
responses={ responses={
200: PolicyTestResultSerializer(), 200: PolicyTestResultSerializer(),
404: OpenApiResponse(description="for_user user not found"), 404: OpenApiResponse(description="for_user user not found"),
}, },
) )
@action(detail=True, methods=["POST"]) @action(detail=True, methods=["GET"])
# pylint: disable=unused-argument # pylint: disable=unused-argument
def check_access(self, request: Request, slug: str) -> Response: def check_access(self, request: Request, slug: str) -> Response:
"""Check access to a single application by slug""" """Check access to a single application by slug"""
@ -203,7 +202,7 @@ class ApplicationViewSet(ModelViewSet):
"""Set application icon""" """Set application icon"""
app: Application = self.get_object() app: Application = self.get_object()
icon = request.FILES.get("file", None) icon = request.FILES.get("file", None)
clear = request.data.get("clear", False) clear = request.data.get("clear", "false").lower() == "true"
if clear: if clear:
# .delete() saves the model by default # .delete() saves the model by default
app.meta_icon.delete() app.meta_icon.delete()

View File

@ -11,6 +11,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from ua_parser import user_agent_parser from ua_parser import user_agent_parser
from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import AuthenticatedSession from authentik.core.models import AuthenticatedSession
from authentik.events.geo import GEOIP_READER, GeoIPDict from authentik.events.geo import GEOIP_READER, GeoIPDict
@ -92,6 +93,7 @@ class AuthenticatedSessionSerializer(ModelSerializer):
class AuthenticatedSessionViewSet( class AuthenticatedSessionViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -5,6 +5,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import is_dict from authentik.core.api.utils import is_dict
from authentik.core.models import Group from authentik.core.models import Group
@ -20,7 +21,7 @@ class GroupSerializer(ModelSerializer):
fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"] fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"]
class GroupViewSet(ModelViewSet): class GroupViewSet(UsedByMixin, ModelViewSet):
"""Group Viewset""" """Group Viewset"""
queryset = Group.objects.all() queryset = Group.objects.all()

View File

@ -14,6 +14,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ( from authentik.core.api.utils import (
MetaNameSerializer, MetaNameSerializer,
PassiveSerializer, PassiveSerializer,
@ -65,6 +66,7 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
class PropertyMappingViewSet( class PropertyMappingViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -9,6 +9,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.models import Provider from authentik.core.models import Provider
from authentik.lib.utils.reflection import all_subclasses from authentik.lib.utils.reflection import all_subclasses
@ -48,6 +49,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
class ProviderViewSet( class ProviderViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -10,6 +10,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.models import Source from authentik.core.models import Source
from authentik.core.types import UserSettingSerializer from authentik.core.types import UserSettingSerializer
@ -52,6 +53,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
class SourceViewSet( class SourceViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -9,6 +9,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Token, TokenIntents from authentik.core.models import Token, TokenIntents
@ -43,7 +44,7 @@ class TokenViewSerializer(PassiveSerializer):
key = CharField(read_only=True) key = CharField(read_only=True)
class TokenViewSet(ModelViewSet): class TokenViewSet(UsedByMixin, ModelViewSet):
"""Token Viewset""" """Token Viewset"""
lookup_field = "identifier" lookup_field = "identifier"

View File

@ -0,0 +1,102 @@
"""used_by mixin"""
from enum import Enum
from inspect import getmembers
from django.db.models.base import Model
from django.db.models.deletion import SET_DEFAULT, SET_NULL
from django.db.models.manager import Manager
from drf_spectacular.utils import extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import CharField, ChoiceField
from rest_framework.request import Request
from rest_framework.response import Response
from authentik.core.api.utils import PassiveSerializer
class DeleteAction(Enum):
"""Which action a delete will have on a used object"""
CASCADE = "cascade"
CASCADE_MANY = "cascade_many"
SET_NULL = "set_null"
SET_DEFAULT = "set_default"
class UsedBySerializer(PassiveSerializer):
"""A list of all objects referencing the queried object"""
app = CharField()
model_name = CharField()
pk = CharField()
name = CharField()
action = ChoiceField(choices=[(x.name, x.name) for x in DeleteAction])
def get_delete_action(manager: Manager) -> str:
"""Get the delete action from the Foreign key, falls back to cascade"""
if hasattr(manager, "field"):
if manager.field.remote_field.on_delete.__name__ == SET_NULL.__name__:
return DeleteAction.SET_NULL.name
if manager.field.remote_field.on_delete.__name__ == SET_DEFAULT.__name__:
return DeleteAction.SET_DEFAULT.name
if hasattr(manager, "source_field"):
return DeleteAction.CASCADE_MANY.name
return DeleteAction.CASCADE.name
class UsedByMixin:
"""Mixin to add a used_by endpoint to return a list of all objects using this object"""
@extend_schema(
responses={200: UsedBySerializer(many=True)},
)
@action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=invalid-name, unused-argument, too-many-locals
def used_by(self, request: Request, *args, **kwargs) -> Response:
"""Get a list of all objects that use this object"""
# pyright: reportGeneralTypeIssues=false
model: Model = self.get_object()
used_by = []
shadows = []
for attr_name, manager in getmembers(model, lambda x: isinstance(x, Manager)):
if attr_name == "objects": # pragma: no cover
continue
manager: Manager
if manager.model._meta.abstract:
continue
app = manager.model._meta.app_label
model_name = manager.model._meta.model_name
delete_action = get_delete_action(manager)
# To make sure we only apply shadows when there are any objects,
# but so we only apply them once, have a simple flag for the first object
first_object = True
for obj in get_objects_for_user(
request.user, f"{app}.view_{model_name}", manager
).all():
# Only merge shadows on first object
if first_object:
shadows += getattr(
manager.model._meta, "authentik_used_by_shadows", []
)
first_object = False
serializer = UsedBySerializer(
data={
"app": app,
"model_name": model_name,
"pk": str(obj.pk),
"name": str(obj),
"action": delete_action,
}
)
serializer.is_valid()
used_by.append(serializer.data)
# Check the shadows map and remove anything that should be shadowed
for idx, user in enumerate(used_by):
full_model_name = f"{user['app']}.{user['model_name']}"
if full_model_name in shadows:
del used_by[idx]
return Response(used_by)

View File

@ -25,6 +25,7 @@ from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.groups import GroupSerializer from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
from authentik.core.middleware import ( from authentik.core.middleware import (
SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_ORIGINAL_USER,
@ -131,7 +132,7 @@ class UsersFilter(FilterSet):
fields = ["username", "name", "is_active", "is_superuser", "attributes"] fields = ["username", "name", "is_active", "is_superuser", "attributes"]
class UserViewSet(ModelViewSet): class UserViewSet(UsedByMixin, ModelViewSet):
"""User Viewset""" """User Viewset"""
queryset = User.objects.none() queryset = User.objects.none()

View File

@ -26,6 +26,8 @@ class ImpersonateMiddleware:
if SESSION_IMPERSONATE_USER in request.session: if SESSION_IMPERSONATE_USER in request.session:
request.user = request.session[SESSION_IMPERSONATE_USER] request.user = request.session[SESSION_IMPERSONATE_USER]
# Ensure that the user is active, otherwise nothing will work
request.user.is_active = True
return self.get_response(request) return self.get_response(request)

View File

@ -5,6 +5,7 @@ from typing import Any, Optional, Type
from urllib.parse import urlencode from urllib.parse import urlencode
from uuid import uuid4 from uuid import uuid4
import django.db.models.options as options
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager from django.contrib.auth.models import UserManager as DjangoUserManager
@ -41,6 +42,9 @@ GRAVATAR_URL = "https://secure.gravatar.com"
DEFAULT_AVATAR = static("dist/assets/images/user_default.png") DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
options.DEFAULT_NAMES = options.DEFAULT_NAMES + ("authentik_used_by_shadows",)
def default_token_duration(): def default_token_duration():
"""Default duration a Token is valid""" """Default duration a Token is valid"""
return now() + timedelta(minutes=30) return now() + timedelta(minutes=30)

View File

@ -1,11 +1,12 @@
"""authentik core signals""" """authentik core signals"""
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Type
from django.contrib.auth.signals import user_logged_in, user_logged_out from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache from django.core.cache import cache
from django.core.signals import Signal from django.core.signals import Signal
from django.db.models import Model from django.db.models import Model
from django.db.models.signals import post_save from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.http.request import HttpRequest from django.http.request import HttpRequest
from prometheus_client import Gauge from prometheus_client import Gauge
@ -18,7 +19,7 @@ GAUGE_MODELS = Gauge(
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from authentik.core.models import User from authentik.core.models import AuthenticatedSession, User
@receiver(post_save) @receiver(post_save)
@ -60,3 +61,17 @@ def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
AuthenticatedSession.objects.filter( AuthenticatedSession.objects.filter(
session_key=request.session.session_key session_key=request.session.session_key
).delete() ).delete()
@receiver(pre_delete)
def authenticated_session_delete(
sender: Type[Model], instance: "AuthenticatedSession", **_
):
"""Delete session when authenticated session is deleted"""
from authentik.core.models import AuthenticatedSession
if sender != AuthenticatedSession:
return
cache_key = f"{KEY_PREFIX}{instance.session_key}"
cache.delete(cache_key)

View File

@ -33,6 +33,7 @@ from authentik.flows.planner import (
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.utils import delete_none_keys from authentik.policies.utils import delete_none_keys
from authentik.stages.password import BACKEND_DJANGO
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
@ -198,7 +199,7 @@ class SourceFlowManager:
kwargs.update( kwargs.update(
{ {
# Since we authenticate the user by their token, they have no backend set # Since we authenticate the user by their token, they have no backend set
PLAN_CONTEXT_AUTHENTICATION_BACKEND: "django.contrib.auth.backends.ModelBackend", PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
PLAN_CONTEXT_SSO: True, PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_SOURCE: self.source, PLAN_CONTEXT_SOURCE: self.source,
PLAN_CONTEXT_REDIRECT: final_redirect, PLAN_CONTEXT_REDIRECT: final_redirect,

View File

@ -3,16 +3,6 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% block head %}
{{ block.super }}
<style>
.pf-c-background-image::before {
background-image: url("{% static 'dist/assets/images/flow_background.jpg' %}");
background-position: center;
}
</style>
{% endblock %}
{% block title %} {% block title %}
{% trans 'End session' %} - {{ tenant.branding_title }} {% trans 'End session' %} - {{ tenant.branding_title }}
{% endblock %} {% endblock %}

View File

@ -26,7 +26,7 @@ class TestApplicationsAPI(APITestCase):
def test_check_access(self): def test_check_access(self):
"""Test check_access operation""" """Test check_access operation"""
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.post( response = self.client.get(
reverse( reverse(
"authentik_api:application-check-access", "authentik_api:application-check-access",
kwargs={"slug": self.allowed.slug}, kwargs={"slug": self.allowed.slug},
@ -36,7 +36,7 @@ class TestApplicationsAPI(APITestCase):
self.assertJSONEqual( self.assertJSONEqual(
force_str(response.content), {"messages": [], "passing": True} force_str(response.content), {"messages": [], "passing": True}
) )
response = self.client.post( response = self.client.get(
reverse( reverse(
"authentik_api:application-check-access", "authentik_api:application-check-access",
kwargs={"slug": self.denied.slug}, kwargs={"slug": self.denied.slug},

View File

@ -17,6 +17,9 @@ class TestImpersonation(TestCase):
def test_impersonate_simple(self): def test_impersonate_simple(self):
"""test simple impersonation and un-impersonation""" """test simple impersonation and un-impersonation"""
# test with an inactive user to ensure that still works
self.other_user.is_active = False
self.other_user.save()
self.client.force_login(self.akadmin) self.client.force_login(self.akadmin)
self.client.get( self.client.get(

View File

@ -2,7 +2,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from rest_framework.fields import CharField, DictField from rest_framework.fields import CharField
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import Challenge from authentik.flows.challenge import Challenge
@ -22,18 +22,10 @@ class UILoginButton:
icon_url: Optional[str] = None icon_url: Optional[str] = None
class UILoginButtonSerializer(PassiveSerializer):
"""Serializer for Login buttons of sources"""
name = CharField()
challenge = DictField()
icon_url = CharField(required=False, allow_null=True)
class UserSettingSerializer(PassiveSerializer): class UserSettingSerializer(PassiveSerializer):
"""Serializer for User settings for stages and sources""" """Serializer for User settings for stages and sources"""
object_uid = CharField() object_uid = CharField()
component = CharField() component = CharField()
title = CharField() title = CharField()
configure_url = CharField() configure_url = CharField(required=False)

View File

@ -1,10 +1,12 @@
"""Crypto API Views""" """Crypto API Views"""
import django_filters
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import load_pem_x509_certificate from cryptography.x509 import load_pem_x509_certificate
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_filters import FilterSet
from django_filters.filters import BooleanFilter
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework.decorators import action from rest_framework.decorators import action
@ -20,6 +22,7 @@ from rest_framework.serializers import ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.builder import CertificateBuilder from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
@ -33,6 +36,9 @@ class CertificateKeyPairSerializer(ModelSerializer):
cert_subject = SerializerMethodField() cert_subject = SerializerMethodField()
private_key_available = SerializerMethodField() private_key_available = SerializerMethodField()
certificate_download_url = SerializerMethodField()
private_key_download_url = SerializerMethodField()
def get_cert_subject(self, instance: CertificateKeyPair) -> str: def get_cert_subject(self, instance: CertificateKeyPair) -> str:
"""Get certificate subject as full rfc4514""" """Get certificate subject as full rfc4514"""
return instance.certificate.subject.rfc4514_string() return instance.certificate.subject.rfc4514_string()
@ -41,6 +47,26 @@ class CertificateKeyPairSerializer(ModelSerializer):
"""Show if this keypair has a private key configured or not""" """Show if this keypair has a private key configured or not"""
return instance.key_data != "" and instance.key_data is not None return instance.key_data != "" and instance.key_data is not None
def get_certificate_download_url(self, instance: CertificateKeyPair) -> str:
"""Get URL to download certificate"""
return (
reverse(
"authentik_api:certificatekeypair-view-certificate",
kwargs={"pk": instance.pk},
)
+ "?download"
)
def get_private_key_download_url(self, instance: CertificateKeyPair) -> str:
"""Get URL to download private key"""
return (
reverse(
"authentik_api:certificatekeypair-view-private-key",
kwargs={"pk": instance.pk},
)
+ "?download"
)
def validate_certificate_data(self, value: str) -> str: def validate_certificate_data(self, value: str) -> str:
"""Verify that input is a valid PEM x509 Certificate""" """Verify that input is a valid PEM x509 Certificate"""
try: try:
@ -77,6 +103,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
"cert_expiry", "cert_expiry",
"cert_subject", "cert_subject",
"private_key_available", "private_key_available",
"certificate_download_url",
"private_key_download_url",
] ]
extra_kwargs = { extra_kwargs = {
"key_data": {"write_only": True}, "key_data": {"write_only": True},
@ -100,10 +128,10 @@ class CertificateGenerationSerializer(PassiveSerializer):
validity_days = IntegerField(initial=365) validity_days = IntegerField(initial=365)
class CertificateKeyPairFilter(django_filters.FilterSet): class CertificateKeyPairFilter(FilterSet):
"""Filter for certificates""" """Filter for certificates"""
has_key = django_filters.BooleanFilter( has_key = BooleanFilter(
label="Only return certificate-key pairs with keys", method="filter_has_key" label="Only return certificate-key pairs with keys", method="filter_has_key"
) )
@ -117,7 +145,7 @@ class CertificateKeyPairFilter(django_filters.FilterSet):
fields = ["name"] fields = ["name"]
class CertificateKeyPairViewSet(ModelViewSet): class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
"""CertificateKeyPair Viewset""" """CertificateKeyPair Viewset"""
queryset = CertificateKeyPair.objects.all() queryset = CertificateKeyPair.objects.all()

View File

@ -4,10 +4,14 @@ import datetime
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from authentik.core.api.used_by import DeleteAction
from authentik.core.models import User from authentik.core.models import User
from authentik.crypto.api import CertificateKeyPairSerializer from authentik.crypto.api import CertificateKeyPairSerializer
from authentik.crypto.builder import CertificateBuilder from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.providers.oauth2.generators import generate_client_secret
from authentik.providers.oauth2.models import OAuth2Provider
class TestCrypto(TestCase): class TestCrypto(TestCase):
@ -91,3 +95,35 @@ class TestCrypto(TestCase):
) )
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
self.assertIn("Content-Disposition", response) self.assertIn("Content-Disposition", response)
def test_used_by(self):
"""Test used_by endpoint"""
self.client.force_login(User.objects.get(username="akadmin"))
keypair = CertificateKeyPair.objects.first()
provider = OAuth2Provider.objects.create(
name="test",
client_id="test",
client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(),
redirect_uris="http://localhost",
rsa_key=CertificateKeyPair.objects.first(),
)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-used-by",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(200, response.status_code)
self.assertJSONEqual(
response.content.decode(),
[
{
"app": "authentik_providers_oauth2",
"model_name": "oauth2provider",
"pk": str(provider.pk),
"name": str(provider),
"action": DeleteAction.SET_NULL.name,
}
],
)

View File

@ -36,6 +36,7 @@ class EventSerializer(ModelSerializer):
"client_ip", "client_ip",
"created", "created",
"expires", "expires",
"tenant",
] ]
@ -76,6 +77,11 @@ class EventsFilter(django_filters.FilterSet):
field_name="action", field_name="action",
lookup_expr="icontains", lookup_expr="icontains",
) )
tenant_name = django_filters.CharFilter(
field_name="tenant",
lookup_expr="name",
label="Tenant name",
)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def filter_context_model_pk(self, queryset, name, value): def filter_context_model_pk(self, queryset, name, value):

View File

@ -7,6 +7,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.events.api.event import EventSerializer from authentik.events.api.event import EventSerializer
from authentik.events.models import Notification from authentik.events.models import Notification
@ -35,6 +36,7 @@ class NotificationViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -3,6 +3,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.groups import GroupSerializer from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.events.models import NotificationRule from authentik.events.models import NotificationRule
@ -24,7 +25,7 @@ class NotificationRuleSerializer(ModelSerializer):
] ]
class NotificationRuleViewSet(ModelViewSet): class NotificationRuleViewSet(UsedByMixin, ModelViewSet):
"""NotificationRule Viewset""" """NotificationRule Viewset"""
queryset = NotificationRule.objects.all() queryset = NotificationRule.objects.all()

View File

@ -9,6 +9,7 @@ from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.events.models import ( from authentik.events.models import (
Notification, Notification,
NotificationSeverity, NotificationSeverity,
@ -52,7 +53,7 @@ class NotificationTransportTestSerializer(Serializer):
raise NotImplementedError raise NotImplementedError
class NotificationTransportViewSet(ModelViewSet): class NotificationTransportViewSet(UsedByMixin, ModelViewSet):
"""NotificationTransport Viewset""" """NotificationTransport Viewset"""
queryset = NotificationTransport.objects.all() queryset = NotificationTransport.objects.all()

View File

@ -40,9 +40,9 @@ class GeoIPReader:
return return
try: try:
reader = Reader(path) reader = Reader(path)
LOGGER.info("Loaded GeoIP database")
self.__reader = reader self.__reader = reader
self.__last_mtime = stat(path).st_mtime self.__last_mtime = stat(path).st_mtime
LOGGER.info("Loaded GeoIP database", last_write=self.__last_mtime)
except OSError as exc: except OSError as exc:
LOGGER.warning("Failed to load GeoIP database", exc=exc) LOGGER.warning("Failed to load GeoIP database", exc=exc)

View File

@ -2,6 +2,7 @@
from functools import partial from functools import partial
from typing import Callable from typing import Callable
from django.conf import settings
from django.db.models import Model from django.db.models import Model
from django.db.models.signals import post_save, pre_delete from django.db.models.signals import post_save, pre_delete
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
@ -12,6 +13,7 @@ from authentik.core.models import User
from authentik.events.models import Event, EventAction, Notification from authentik.events.models import Event, EventAction, Notification
from authentik.events.signals import EventNewThread from authentik.events.signals import EventNewThread
from authentik.events.utils import model_to_dict from authentik.events.utils import model_to_dict
from authentik.lib.utils.errors import exception_to_string
class AuditMiddleware: class AuditMiddleware:
@ -54,10 +56,19 @@ class AuditMiddleware:
# pylint: disable=unused-argument # pylint: disable=unused-argument
def process_exception(self, request: HttpRequest, exception: Exception): def process_exception(self, request: HttpRequest, exception: Exception):
"""Unregister handlers in case of exception""" """Disconnect handlers in case of exception"""
post_save.disconnect(dispatch_uid=LOCAL.authentik["request_id"]) post_save.disconnect(dispatch_uid=LOCAL.authentik["request_id"])
pre_delete.disconnect(dispatch_uid=LOCAL.authentik["request_id"]) pre_delete.disconnect(dispatch_uid=LOCAL.authentik["request_id"])
if settings.DEBUG:
return
thread = EventNewThread(
EventAction.SYSTEM_EXCEPTION,
request,
message=exception_to_string(exception),
)
thread.run()
@staticmethod @staticmethod
# pylint: disable=unused-argument # pylint: disable=unused-argument
def post_save_handler( def post_save_handler(

View File

@ -0,0 +1,55 @@
# Generated by Django 3.2.4 on 2021-06-14 15:33
from django.db import migrations, models
import authentik.events.models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0015_alter_event_action"),
]
operations = [
migrations.AddField(
model_name="event",
name="tenant",
field=models.JSONField(
blank=True, default=authentik.events.models.default_tenant
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("system_exception", "System Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("email_sent", "Email Sent"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
]

View File

@ -21,11 +21,12 @@ from authentik.core.middleware import (
) )
from authentik.core.models import ExpiringModel, Group, User from authentik.core.models import ExpiringModel, Group, User
from authentik.events.geo import GEOIP_READER from authentik.events.geo import GEOIP_READER
from authentik.events.utils import cleanse_dict, get_user, sanitize_dict from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.http import get_client_ip from authentik.lib.utils.http import get_client_ip
from authentik.policies.models import PolicyBindingModel from authentik.policies.models import PolicyBindingModel
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
from authentik.tenants.utils import DEFAULT_TENANT
LOGGER = get_logger("authentik.events") LOGGER = get_logger("authentik.events")
GAUGE_EVENTS = Gauge( GAUGE_EVENTS = Gauge(
@ -40,6 +41,11 @@ def default_event_duration():
return now() + timedelta(days=365) return now() + timedelta(days=365)
def default_tenant():
"""Get a default value for tenant"""
return sanitize_dict(model_to_dict(DEFAULT_TENANT))
class NotificationTransportError(SentryIgnoredException): class NotificationTransportError(SentryIgnoredException):
"""Error raised when a notification fails to be delivered""" """Error raised when a notification fails to be delivered"""
@ -71,6 +77,7 @@ class EventAction(models.TextChoices):
SYSTEM_TASK_EXECUTION = "system_task_execution" SYSTEM_TASK_EXECUTION = "system_task_execution"
SYSTEM_TASK_EXCEPTION = "system_task_exception" SYSTEM_TASK_EXCEPTION = "system_task_exception"
SYSTEM_EXCEPTION = "system_exception"
CONFIGURATION_ERROR = "configuration_error" CONFIGURATION_ERROR = "configuration_error"
@ -94,6 +101,7 @@ class Event(ExpiringModel):
context = models.JSONField(default=dict, blank=True) context = models.JSONField(default=dict, blank=True)
client_ip = models.GenericIPAddressField(null=True) client_ip = models.GenericIPAddressField(null=True)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
tenant = models.JSONField(default=default_tenant, blank=True)
# Shadow the expires attribute from ExpiringModel to override the default duration # Shadow the expires attribute from ExpiringModel to override the default duration
expires = models.DateTimeField(default=default_event_duration) expires = models.DateTimeField(default=default_event_duration)
@ -132,6 +140,13 @@ class Event(ExpiringModel):
"""Add data from a Django-HttpRequest, allowing the creation of """Add data from a Django-HttpRequest, allowing the creation of
Events independently from requests. Events independently from requests.
`user` arguments optionally overrides user from requests.""" `user` arguments optionally overrides user from requests."""
if request:
self.context["http_request"] = {
"path": request.get_full_path(),
"method": request.method,
}
if hasattr(request, "tenant"):
self.tenant = sanitize_dict(model_to_dict(request.tenant))
if hasattr(request, "user"): if hasattr(request, "user"):
original_user = None original_user = None
if hasattr(request, "session"): if hasattr(request, "session"):

View File

@ -2,6 +2,7 @@
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
from authentik.flows.models import FlowStageBinding from authentik.flows.models import FlowStageBinding
@ -27,7 +28,7 @@ class FlowStageBindingSerializer(ModelSerializer):
] ]
class FlowStageBindingViewSet(ModelViewSet): class FlowStageBindingViewSet(UsedByMixin, ModelViewSet):
"""FlowStageBinding Viewset""" """FlowStageBinding Viewset"""
queryset = FlowStageBinding.objects.all() queryset = FlowStageBinding.objects.all()

View File

@ -24,6 +24,7 @@ from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import CacheSerializer, LinkSerializer from authentik.core.api.utils import CacheSerializer, LinkSerializer
from authentik.flows.exceptions import FlowNonApplicableException from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow from authentik.flows.models import Flow
@ -44,10 +45,16 @@ class FlowSerializer(ModelSerializer):
background = ReadOnlyField(source="background_url") background = ReadOnlyField(source="background_url")
export_url = SerializerMethodField()
def get_cache_count(self, flow: Flow) -> int: def get_cache_count(self, flow: Flow) -> int:
"""Get count of cached flows""" """Get count of cached flows"""
return len(cache.keys(f"{cache_key(flow)}*")) return len(cache.keys(f"{cache_key(flow)}*"))
def get_export_url(self, flow: Flow) -> str:
"""Get export URL for flow"""
return reverse("authentik_api:flow-export", kwargs={"slug": flow.slug})
class Meta: class Meta:
model = Flow model = Flow
@ -64,6 +71,7 @@ class FlowSerializer(ModelSerializer):
"cache_count", "cache_count",
"policy_engine_mode", "policy_engine_mode",
"compatibility_mode", "compatibility_mode",
"export_url",
] ]
extra_kwargs = { extra_kwargs = {
"background": {"read_only": True}, "background": {"read_only": True},
@ -94,7 +102,7 @@ class DiagramElement:
return f"{self.identifier}=>{self.type}: {self.rest}" return f"{self.identifier}=>{self.type}: {self.rest}"
class FlowViewSet(ModelViewSet): class FlowViewSet(UsedByMixin, ModelViewSet):
"""Flow Viewset""" """Flow Viewset"""
queryset = Flow.objects.all() queryset = Flow.objects.all()
@ -293,10 +301,14 @@ class FlowViewSet(ModelViewSet):
"""Set Flow background""" """Set Flow background"""
flow: Flow = self.get_object() flow: Flow = self.get_object()
background = request.FILES.get("file", None) background = request.FILES.get("file", None)
clear = request.data.get("clear", False) clear = request.data.get("clear", "false").lower() == "true"
if clear: if clear:
if flow.background_url.startswith("/media"):
# .delete() saves the model by default # .delete() saves the model by default
flow.background.delete() flow.background.delete()
else:
flow.background = None
flow.save()
return Response({}) return Response({})
if background: if background:
flow.background = background flow.background = background

View File

@ -11,6 +11,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.types import UserSettingSerializer from authentik.core.types import UserSettingSerializer
from authentik.flows.api.flows import FlowSerializer from authentik.flows.api.flows import FlowSerializer
@ -49,6 +50,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
class StageViewSet( class StageViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):
@ -91,10 +93,10 @@ class StageViewSet(
if not user_settings: if not user_settings:
continue continue
user_settings.initial_data["object_uid"] = str(stage.pk) user_settings.initial_data["object_uid"] = str(stage.pk)
if hasattr(stage, "configure_url"): if hasattr(stage, "configure_flow") and stage.configure_flow:
user_settings.initial_data["configure_url"] = reverse( user_settings.initial_data["configure_url"] = reverse(
"authentik_flows:configure", "authentik_flows:configure",
kwargs={"stage_uuid": stage.uuid.hex}, kwargs={"stage_uuid": stage.pk},
) )
if not user_settings.is_valid(): if not user_settings.is_valid():
LOGGER.warning(user_settings.errors) LOGGER.warning(user_settings.errors)

View File

@ -6,6 +6,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.flows.models import FlowDesignation from authentik.flows.models import FlowDesignation
from authentik.stages.identification.models import UserFields from authentik.stages.identification.models import UserFields
from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP
def create_default_authentication_flow( def create_default_authentication_flow(
@ -31,7 +32,7 @@ def create_default_authentication_flow(
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create( password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
name="default-authentication-password", name="default-authentication-password",
defaults={"backends": ["django.contrib.auth.backends.ModelBackend"]}, defaults={"backends": [BACKEND_DJANGO, BACKEND_LDAP]},
) )
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create( login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(

View File

@ -15,9 +15,6 @@ PREFILL_POLICY_EXPRESSION = """# This policy sets the user for the currently run
# by injecting "pending_user" # by injecting "pending_user"
akadmin = ak_user_by(username="akadmin") akadmin = ak_user_by(username="akadmin")
context["pending_user"] = akadmin context["pending_user"] = akadmin
# We're also setting the backend for the user, so we can
# directly login without having to identify again
context["user_backend"] = "django.contrib.auth.backends.ModelBackend"
return True""" return True"""

View File

@ -72,7 +72,7 @@ class Stage(SerializerModel):
def __str__(self): def __str__(self):
if hasattr(self, "__in_memory_type"): if hasattr(self, "__in_memory_type"):
return f"In-memory Stage {getattr(self, '__in_memory_type')}" return f"In-memory Stage {getattr(self, '__in_memory_type')}"
return self.name return f"Stage {self.name}"
def in_memory_stage(view: Type["StageView"]) -> Stage: def in_memory_stage(view: Type["StageView"]) -> Stage:
@ -212,7 +212,7 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
return FlowStageBindingSerializer return FlowStageBindingSerializer
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.target} #{self.order}" return f"Flow-stage binding #{self.order} to {self.target}"
class Meta: class Meta:

View File

@ -50,14 +50,17 @@ class StageView(View):
self.executor = executor self.executor = executor
super().__init__(**kwargs) super().__init__(**kwargs)
def get_pending_user(self) -> User: def get_pending_user(self, for_display=False) -> User:
"""Either show the matched User object or show what the user entered, """Either show the matched User object or show what the user entered,
based on what the earlier stage (mostly IdentificationStage) set. based on what the earlier stage (mostly IdentificationStage) set.
_USER_IDENTIFIER overrides the first User, as PENDING_USER is used for _USER_IDENTIFIER overrides the first User, as PENDING_USER is used for
other things besides the form display. other things besides the form display.
If no user is pending, returns request.user""" If no user is pending, returns request.user"""
if PLAN_CONTEXT_PENDING_USER_IDENTIFIER in self.executor.plan.context: if (
PLAN_CONTEXT_PENDING_USER_IDENTIFIER in self.executor.plan.context
and for_display
):
return User( return User(
username=self.executor.plan.context.get( username=self.executor.plan.context.get(
PLAN_CONTEXT_PENDING_USER_IDENTIFIER PLAN_CONTEXT_PENDING_USER_IDENTIFIER
@ -109,7 +112,7 @@ class ChallengeStageView(StageView):
# If there's a pending user, update the `username` field # If there's a pending user, update the `username` field
# this field is only used by password managers. # this field is only used by password managers.
# If there's no user set, an error is raised later. # If there's no user set, an error is raised later.
if user := self.get_pending_user(): if user := self.get_pending_user(for_display=True):
challenge.initial_data["pending_user"] = user.username challenge.initial_data["pending_user"] = user.username
challenge.initial_data["pending_user_avatar"] = DEFAULT_AVATAR challenge.initial_data["pending_user_avatar"] = DEFAULT_AVATAR
if not isinstance(user, AnonymousUser): if not isinstance(user, AnonymousUser):

View File

@ -511,4 +511,4 @@ class TestFlowExecutor(TestCase):
executor.flow = flow executor.flow = flow
stage_view = StageView(executor) stage_view = StageView(executor)
self.assertEqual(ident, stage_view.get_pending_user().username) self.assertEqual(ident, stage_view.get_pending_user(for_display=True).username)

View File

@ -174,7 +174,7 @@ class FlowExecutorView(APIView):
@extend_schema( @extend_schema(
responses={ responses={
200: PolymorphicProxySerializer( 200: PolymorphicProxySerializer(
component_name="FlowChallengeRequest", component_name="ChallengeTypes",
serializers=challenge_types(), serializers=challenge_types(),
resource_type_field_name="component", resource_type_field_name="component",
), ),
@ -214,7 +214,7 @@ class FlowExecutorView(APIView):
@extend_schema( @extend_schema(
responses={ responses={
200: PolymorphicProxySerializer( 200: PolymorphicProxySerializer(
component_name="FlowChallengeRequest", component_name="ChallengeTypes",
serializers=challenge_types(), serializers=challenge_types(),
resource_type_field_name="component", resource_type_field_name="component",
), ),

View File

@ -0,0 +1,10 @@
"""error utils"""
from traceback import format_tb
TRACEBACK_HEADER = "Traceback (most recent call last):\n"
def exception_to_string(exc: Exception) -> str:
"""Convert exception to string stackrace"""
# Either use passed original exception or whatever we have
return TRACEBACK_HEADER + "".join(format_tb(exc.__traceback__)) + str(exc)

View File

@ -11,6 +11,7 @@ from rest_framework.serializers import JSONField, ModelSerializer, ValidationErr
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer, is_dict from authentik.core.api.utils import PassiveSerializer, is_dict
from authentik.core.models import Provider from authentik.core.models import Provider
from authentik.outposts.api.service_connections import ServiceConnectionSerializer from authentik.outposts.api.service_connections import ServiceConnectionSerializer
@ -95,7 +96,7 @@ class OutpostHealthSerializer(PassiveSerializer):
version_outdated = BooleanField(read_only=True) version_outdated = BooleanField(read_only=True)
class OutpostViewSet(ModelViewSet): class OutpostViewSet(UsedByMixin, ModelViewSet):
"""Outpost Viewset""" """Outpost Viewset"""
queryset = Outpost.objects.all() queryset = Outpost.objects.all()

View File

@ -14,6 +14,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ( from authentik.core.api.utils import (
MetaNameSerializer, MetaNameSerializer,
PassiveSerializer, PassiveSerializer,
@ -32,6 +33,13 @@ class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
component = ReadOnlyField() component = ReadOnlyField()
def get_component(self, obj: OutpostServiceConnection) -> str:
"""Get object type so that we know how to edit the object"""
# pyright: reportGeneralTypeIssues=false
if obj.__class__ == OutpostServiceConnection:
return ""
return obj.component
class Meta: class Meta:
model = OutpostServiceConnection model = OutpostServiceConnection
@ -55,6 +63,7 @@ class ServiceConnectionStateSerializer(PassiveSerializer):
class ServiceConnectionViewSet( class ServiceConnectionViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):
@ -105,7 +114,7 @@ class DockerServiceConnectionSerializer(ServiceConnectionSerializer):
] ]
class DockerServiceConnectionViewSet(ModelViewSet): class DockerServiceConnectionViewSet(UsedByMixin, ModelViewSet):
"""DockerServiceConnection Viewset""" """DockerServiceConnection Viewset"""
queryset = DockerServiceConnection.objects.all() queryset = DockerServiceConnection.objects.all()
@ -139,7 +148,7 @@ class KubernetesServiceConnectionSerializer(ServiceConnectionSerializer):
fields = ServiceConnectionSerializer.Meta.fields + ["kubeconfig"] fields = ServiceConnectionSerializer.Meta.fields + ["kubeconfig"]
class KubernetesServiceConnectionViewSet(ModelViewSet): class KubernetesServiceConnectionViewSet(UsedByMixin, ModelViewSet):
"""KubernetesServiceConnection Viewset""" """KubernetesServiceConnection Viewset"""
queryset = KubernetesServiceConnection.objects.all() queryset = KubernetesServiceConnection.objects.all()

View File

@ -63,7 +63,7 @@ class DockerController(BaseController):
self.client.images.pull(image_name) self.client.images.pull(image_name)
container_args = { container_args = {
"image": image_name, "image": image_name,
"name": f"authentik-proxy-{self.outpost.uuid.hex}", "name": container_name,
"detach": True, "detach": True,
"ports": { "ports": {
f"{port.port}/{port.protocol.lower()}": port.inner_port or port.port f"{port.port}/{port.protocol.lower()}": port.inner_port or port.port

View File

@ -8,7 +8,7 @@ from uuid import uuid4
from dacite import from_dict from dacite import from_dict
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.core.cache import cache from django.core.cache import cache
from django.db import models, transaction from django.db import IntegrityError, models, transaction
from django.db.models.base import Model from django.db.models.base import Model
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from docker.client import DockerClient from docker.client import DockerClient
@ -50,6 +50,8 @@ class ServiceConnectionInvalid(SentryIgnoredException):
class OutpostConfig: class OutpostConfig:
"""Configuration an outpost uses to configure it self""" """Configuration an outpost uses to configure it self"""
# update website/docs/outposts/outposts.md
authentik_host: str authentik_host: str
authentik_host_insecure: bool = False authentik_host_insecure: bool = False
@ -141,7 +143,9 @@ class OutpostServiceConnection(models.Model):
@property @property
def component(self) -> str: def component(self) -> str:
"""Return component used to edit this object""" """Return component used to edit this object"""
raise NotImplementedError # This is called when creating an outpost with a service connection
# since the response doesn't use the correct inheritance
return ""
class Meta: class Meta:
@ -380,13 +384,11 @@ class Outpost(models.Model):
tokens = Token.filter_not_expired( tokens = Token.filter_not_expired(
identifier=self.token_identifier, identifier=self.token_identifier,
intent=TokenIntents.INTENT_API, intent=TokenIntents.INTENT_API,
managed=managed,
) )
if tokens.exists(): if tokens.exists():
token = tokens.first() return tokens.first()
if not token.managed: try:
token.managed = managed
token.save()
return token
return Token.objects.create( return Token.objects.create(
user=self.user, user=self.user,
identifier=self.token_identifier, identifier=self.token_identifier,
@ -395,6 +397,11 @@ class Outpost(models.Model):
expiring=False, expiring=False,
managed=managed, managed=managed,
) )
except IntegrityError:
# Integrity error happens mostly when managed is re-used
Token.objects.filter(managed=managed).delete()
Token.objects.filter(identifier=self.token_identifier).delete()
return self.token
def get_required_objects(self) -> Iterable[Union[models.Model, str]]: def get_required_objects(self) -> Iterable[Union[models.Model, str]]:
"""Get an iterator of all objects the user needs read access to""" """Get an iterator of all objects the user needs read access to"""

View File

@ -11,6 +11,7 @@ from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.api.groups import GroupSerializer from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer from authentik.core.api.users import UserSerializer
from authentik.policies.api.policies import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.models import PolicyBinding, PolicyBindingModel from authentik.policies.models import PolicyBinding, PolicyBindingModel
@ -99,7 +100,7 @@ class PolicyBindingSerializer(ModelSerializer):
return data return data
class PolicyBindingViewSet(ModelViewSet): class PolicyBindingViewSet(UsedByMixin, ModelViewSet):
"""PolicyBinding Viewset""" """PolicyBinding Viewset"""
queryset = ( queryset = (

View File

@ -14,6 +14,7 @@ from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.applications import user_app_cache_key from authentik.core.api.applications import user_app_cache_key
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ( from authentik.core.api.utils import (
CacheSerializer, CacheSerializer,
MetaNameSerializer, MetaNameSerializer,
@ -79,6 +80,7 @@ class PolicySerializer(ModelSerializer, MetaNameSerializer):
class PolicyViewSet( class PolicyViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -1,6 +1,7 @@
"""Dummy Policy API Views""" """Dummy Policy API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.dummy.models import DummyPolicy from authentik.policies.dummy.models import DummyPolicy
@ -13,7 +14,7 @@ class DummyPolicySerializer(PolicySerializer):
fields = PolicySerializer.Meta.fields + ["result", "wait_min", "wait_max"] fields = PolicySerializer.Meta.fields + ["result", "wait_min", "wait_max"]
class DummyPolicyViewSet(ModelViewSet): class DummyPolicyViewSet(UsedByMixin, ModelViewSet):
"""Dummy Viewset""" """Dummy Viewset"""
queryset = DummyPolicy.objects.all() queryset = DummyPolicy.objects.all()

View File

@ -1,6 +1,7 @@
"""Event Matcher Policy API""" """Event Matcher Policy API"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.event_matcher.models import EventMatcherPolicy from authentik.policies.event_matcher.models import EventMatcherPolicy
@ -17,7 +18,7 @@ class EventMatcherPolicySerializer(PolicySerializer):
] ]
class EventMatcherPolicyViewSet(ModelViewSet): class EventMatcherPolicyViewSet(UsedByMixin, ModelViewSet):
"""Event Matcher Policy Viewset""" """Event Matcher Policy Viewset"""
queryset = EventMatcherPolicy.objects.all() queryset = EventMatcherPolicy.objects.all()

View File

@ -0,0 +1,48 @@
# Generated by Django 3.2.4 on 2021-06-14 15:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_policies_event_matcher", "0016_alter_eventmatcherpolicy_action"),
]
operations = [
migrations.AlterField(
model_name="eventmatcherpolicy",
name="action",
field=models.TextField(
blank=True,
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("system_exception", "System Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("email_sent", "Email Sent"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
],
help_text="Match created events with this action type. When left empty, all action types will be matched.",
),
),
]

View File

@ -1,6 +1,7 @@
"""Password Expiry Policy API Views""" """Password Expiry Policy API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.expiry.models import PasswordExpiryPolicy from authentik.policies.expiry.models import PasswordExpiryPolicy
@ -13,7 +14,7 @@ class PasswordExpiryPolicySerializer(PolicySerializer):
fields = PolicySerializer.Meta.fields + ["days", "deny_only"] fields = PolicySerializer.Meta.fields + ["days", "deny_only"]
class PasswordExpiryPolicyViewSet(ModelViewSet): class PasswordExpiryPolicyViewSet(UsedByMixin, ModelViewSet):
"""Password Expiry Viewset""" """Password Expiry Viewset"""
queryset = PasswordExpiryPolicy.objects.all() queryset = PasswordExpiryPolicy.objects.all()

View File

@ -1,6 +1,7 @@
"""Expression Policy API""" """Expression Policy API"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.expression.evaluator import PolicyEvaluator from authentik.policies.expression.evaluator import PolicyEvaluator
from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.expression.models import ExpressionPolicy
@ -20,7 +21,7 @@ class ExpressionPolicySerializer(PolicySerializer):
fields = PolicySerializer.Meta.fields + ["expression"] fields = PolicySerializer.Meta.fields + ["expression"]
class ExpressionPolicyViewSet(ModelViewSet): class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet):
"""Source Viewset""" """Source Viewset"""
queryset = ExpressionPolicy.objects.all() queryset = ExpressionPolicy.objects.all()

View File

@ -1,6 +1,7 @@
"""Source API Views""" """Source API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.hibp.models import HaveIBeenPwendPolicy from authentik.policies.hibp.models import HaveIBeenPwendPolicy
@ -13,7 +14,7 @@ class HaveIBeenPwendPolicySerializer(PolicySerializer):
fields = PolicySerializer.Meta.fields + ["password_field", "allowed_count"] fields = PolicySerializer.Meta.fields + ["password_field", "allowed_count"]
class HaveIBeenPwendPolicyViewSet(ModelViewSet): class HaveIBeenPwendPolicyViewSet(UsedByMixin, ModelViewSet):
"""Source Viewset""" """Source Viewset"""
queryset = HaveIBeenPwendPolicy.objects.all() queryset = HaveIBeenPwendPolicy.objects.all()

View File

@ -1,6 +1,7 @@
"""Password Policy API Views""" """Password Policy API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.password.models import PasswordPolicy from authentik.policies.password.models import PasswordPolicy
@ -21,7 +22,7 @@ class PasswordPolicySerializer(PolicySerializer):
] ]
class PasswordPolicyViewSet(ModelViewSet): class PasswordPolicyViewSet(UsedByMixin, ModelViewSet):
"""Password Policy Viewset""" """Password Policy Viewset"""
queryset = PasswordPolicy.objects.all() queryset = PasswordPolicy.objects.all()

View File

@ -1,7 +1,6 @@
"""authentik policy task""" """authentik policy task"""
from multiprocessing import get_context from multiprocessing import get_context
from multiprocessing.connection import Connection from multiprocessing.connection import Connection
from traceback import format_tb
from typing import Optional from typing import Optional
from django.core.cache import cache from django.core.cache import cache
@ -11,12 +10,12 @@ from sentry_sdk.tracing import Span
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.exceptions import PolicyException from authentik.policies.exceptions import PolicyException
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()
TRACEBACK_HEADER = "Traceback (most recent call last):\n"
FORK_CTX = get_context("fork") FORK_CTX = get_context("fork")
PROCESS_CLASS = FORK_CTX.Process PROCESS_CLASS = FORK_CTX.Process
@ -106,11 +105,7 @@ class PolicyProcess(PROCESS_CLASS):
except PolicyException as exc: except PolicyException as exc:
# Either use passed original exception or whatever we have # Either use passed original exception or whatever we have
src_exc = exc.src_exc if exc.src_exc else exc src_exc = exc.src_exc if exc.src_exc else exc
error_string = ( error_string = exception_to_string(src_exc)
TRACEBACK_HEADER
+ "".join(format_tb(src_exc.__traceback__))
+ str(src_exc)
)
# Create policy exception event, only when we're not debugging # Create policy exception event, only when we're not debugging
if not self.request.debug: if not self.request.debug:
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string) self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)

View File

@ -3,6 +3,7 @@ from rest_framework import mixins
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.reputation.models import ( from authentik.policies.reputation.models import (
IPReputation, IPReputation,
@ -23,7 +24,7 @@ class ReputationPolicySerializer(PolicySerializer):
] ]
class ReputationPolicyViewSet(ModelViewSet): class ReputationPolicyViewSet(UsedByMixin, ModelViewSet):
"""Reputation Policy Viewset""" """Reputation Policy Viewset"""
queryset = ReputationPolicy.objects.all() queryset = ReputationPolicy.objects.all()
@ -46,6 +47,7 @@ class IPReputationSerializer(ModelSerializer):
class IPReputationViewSet( class IPReputationViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):
@ -74,6 +76,7 @@ class UserReputationSerializer(ModelSerializer):
class UserReputationViewSet( class UserReputationViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -3,11 +3,13 @@ from django.core.cache import cache
from django.db import models from django.db import models
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
from structlog import get_logger
from authentik.lib.utils.http import get_client_ip from authentik.lib.utils.http import get_client_ip
from authentik.policies.models import Policy from authentik.policies.models import Policy
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger()
CACHE_KEY_IP_PREFIX = "authentik_reputation_ip_" CACHE_KEY_IP_PREFIX = "authentik_reputation_ip_"
CACHE_KEY_USER_PREFIX = "authentik_reputation_user_" CACHE_KEY_USER_PREFIX = "authentik_reputation_user_"
@ -35,9 +37,16 @@ class ReputationPolicy(Policy):
if self.check_ip: if self.check_ip:
score = cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0) score = cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0)
passing = passing and score <= self.threshold passing = passing and score <= self.threshold
LOGGER.debug("Score for IP", ip=remote_ip, score=score, passing=passing)
if self.check_username: if self.check_username:
score = cache.get_or_set(CACHE_KEY_USER_PREFIX + request.user.username, 0) score = cache.get_or_set(CACHE_KEY_USER_PREFIX + request.user.username, 0)
passing = passing and score <= self.threshold passing = passing and score <= self.threshold
LOGGER.debug(
"Score for Username",
username=request.user.username,
score=score,
passing=passing,
)
return PolicyResult(passing) return PolicyResult(passing)
class Meta: class Meta:

View File

@ -1,9 +1,10 @@
"""test reputation signals and policy""" """test reputation signals and policy"""
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase from django.test import RequestFactory, TestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.lib.utils.http import DEFAULT_IP
from authentik.policies.reputation.models import ( from authentik.policies.reputation.models import (
CACHE_KEY_IP_PREFIX, CACHE_KEY_IP_PREFIX,
CACHE_KEY_USER_PREFIX, CACHE_KEY_USER_PREFIX,
@ -19,9 +20,12 @@ class TestReputationPolicy(TestCase):
"""test reputation signals and policy""" """test reputation signals and policy"""
def setUp(self): def setUp(self):
self.test_ip = "255.255.255.255" self.request_factory = RequestFactory()
self.request = self.request_factory.get("/")
self.test_ip = "127.0.0.1"
self.test_username = "test" self.test_username = "test"
cache.delete(CACHE_KEY_IP_PREFIX + self.test_ip) cache.delete(CACHE_KEY_IP_PREFIX + self.test_ip)
cache.delete(CACHE_KEY_IP_PREFIX + DEFAULT_IP)
cache.delete(CACHE_KEY_USER_PREFIX + self.test_username) cache.delete(CACHE_KEY_USER_PREFIX + self.test_username)
# We need a user for the one-to-one in userreputation # We need a user for the one-to-one in userreputation
self.user = User.objects.create(username=self.test_username) self.user = User.objects.create(username=self.test_username)
@ -29,7 +33,9 @@ class TestReputationPolicy(TestCase):
def test_ip_reputation(self): def test_ip_reputation(self):
"""test IP reputation""" """test IP reputation"""
# Trigger negative reputation # Trigger negative reputation
authenticate(None, username=self.test_username, password=self.test_username) authenticate(
self.request, username=self.test_username, password=self.test_username
)
# Test value in cache # Test value in cache
self.assertEqual(cache.get(CACHE_KEY_IP_PREFIX + self.test_ip), -1) self.assertEqual(cache.get(CACHE_KEY_IP_PREFIX + self.test_ip), -1)
# Save cache and check db values # Save cache and check db values
@ -39,7 +45,9 @@ class TestReputationPolicy(TestCase):
def test_user_reputation(self): def test_user_reputation(self):
"""test User reputation""" """test User reputation"""
# Trigger negative reputation # Trigger negative reputation
authenticate(None, username=self.test_username, password=self.test_username) authenticate(
self.request, username=self.test_username, password=self.test_username
)
# Test value in cache # Test value in cache
self.assertEqual(cache.get(CACHE_KEY_USER_PREFIX + self.test_username), -1) self.assertEqual(cache.get(CACHE_KEY_USER_PREFIX + self.test_username), -1)
# Save cache and check db values # Save cache and check db values

View File

@ -4,6 +4,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.providers.ldap.models import LDAPProvider from authentik.providers.ldap.models import LDAPProvider
@ -19,7 +20,7 @@ class LDAPProviderSerializer(ProviderSerializer):
] ]
class LDAPProviderViewSet(ModelViewSet): class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
"""LDAPProvider Viewset""" """LDAPProvider Viewset"""
queryset = LDAPProvider.objects.all() queryset = LDAPProvider.objects.all()

View File

@ -11,6 +11,7 @@ from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Provider from authentik.core.models import Provider
from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider
@ -61,7 +62,7 @@ class OAuth2ProviderSetupURLs(PassiveSerializer):
logout = ReadOnlyField() logout = ReadOnlyField()
class OAuth2ProviderViewSet(ModelViewSet): class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
"""OAuth2Provider Viewset""" """OAuth2Provider Viewset"""
queryset = OAuth2Provider.objects.all() queryset = OAuth2Provider.objects.all()

View File

@ -2,6 +2,7 @@
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.propertymappings import PropertyMappingSerializer from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.providers.oauth2.models import ScopeMapping from authentik.providers.oauth2.models import ScopeMapping
@ -17,7 +18,7 @@ class ScopeMappingSerializer(PropertyMappingSerializer):
] ]
class ScopeMappingViewSet(ModelViewSet): class ScopeMappingViewSet(UsedByMixin, ModelViewSet):
"""ScopeMapping Viewset""" """ScopeMapping Viewset"""
queryset = ScopeMapping.objects.all() queryset = ScopeMapping.objects.all()

View File

@ -10,6 +10,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import MetaNameSerializer from authentik.core.api.utils import MetaNameSerializer
from authentik.providers.oauth2.api.provider import OAuth2ProviderSerializer from authentik.providers.oauth2.api.provider import OAuth2ProviderSerializer
@ -57,6 +58,7 @@ class RefreshTokenModelSerializer(ExpiringBaseGrantModelSerializer):
class AuthorizationCodeViewSet( class AuthorizationCodeViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):
@ -82,6 +84,7 @@ class AuthorizationCodeViewSet(
class RefreshTokenViewSet( class RefreshTokenViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -0,0 +1,26 @@
# Generated by Django 3.2.3 on 2021-06-09 21:52
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_crypto", "0002_create_self_signed_kp"),
("authentik_providers_oauth2", "0013_alter_authorizationcode_nonce"),
]
operations = [
migrations.AlterField(
model_name="oauth2provider",
name="rsa_key",
field=models.ForeignKey(
help_text="Key used to sign the tokens. Only required when JWT Algorithm is set to RS256.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_crypto.certificatekeypair",
verbose_name="RSA Key",
),
),
]

View File

@ -215,8 +215,7 @@ class OAuth2Provider(Provider):
rsa_key = models.ForeignKey( rsa_key = models.ForeignKey(
CertificateKeyPair, CertificateKeyPair,
verbose_name=_("RSA Key"), verbose_name=_("RSA Key"),
on_delete=models.CASCADE, on_delete=models.SET_NULL,
blank=True,
null=True, null=True,
help_text=_( help_text=_(
"Key used to sign the tokens. Only required when JWT Algorithm is set to RS256." "Key used to sign the tokens. Only required when JWT Algorithm is set to RS256."

View File

@ -1,13 +1,14 @@
"""ProxyProvider API Views""" """ProxyProvider API Views"""
from typing import Any from typing import Any
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, ListField, SerializerMethodField from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.providers.oauth2.views.provider import ProviderInfoView from authentik.providers.oauth2.views.provider import ProviderInfoView
from authentik.providers.proxy.models import ProxyMode, ProxyProvider from authentik.providers.proxy.models import ProxyMode, ProxyProvider
@ -76,7 +77,7 @@ class ProxyProviderSerializer(ProviderSerializer):
] ]
class ProxyProviderViewSet(ModelViewSet): class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
"""ProxyProvider Viewset""" """ProxyProvider Viewset"""
queryset = ProxyProvider.objects.all() queryset = ProxyProvider.objects.all()
@ -84,6 +85,7 @@ class ProxyProviderViewSet(ModelViewSet):
ordering = ["name"] ordering = ["name"]
@extend_schema_serializer(deprecate_fields=["forward_auth_mode"])
class ProxyOutpostConfigSerializer(ModelSerializer): class ProxyOutpostConfigSerializer(ModelSerializer):
"""Proxy provider serializer for outposts""" """Proxy provider serializer for outposts"""

View File

@ -167,3 +167,4 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
verbose_name = _("Proxy Provider") verbose_name = _("Proxy Provider")
verbose_name_plural = _("Proxy Providers") verbose_name_plural = _("Proxy Providers")
authentik_used_by_shadows = ["authentik_providers_oauth2.oauth2provider"]

View File

@ -4,11 +4,17 @@ from xml.etree.ElementTree import ParseError # nosec
from defusedxml.ElementTree import fromstring from defusedxml.ElementTree import fromstring
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, FileField, ReadOnlyField from rest_framework.fields import (
CharField,
FileField,
ReadOnlyField,
SerializerMethodField,
)
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.relations import SlugRelatedField from rest_framework.relations import SlugRelatedField
@ -21,6 +27,7 @@ from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.propertymappings import PropertyMappingSerializer from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Provider from authentik.core.models import Provider
from authentik.flows.models import Flow, FlowDesignation from authentik.flows.models import Flow, FlowDesignation
@ -36,6 +43,15 @@ LOGGER = get_logger()
class SAMLProviderSerializer(ProviderSerializer): class SAMLProviderSerializer(ProviderSerializer):
"""SAMLProvider Serializer""" """SAMLProvider Serializer"""
metadata_download_url = SerializerMethodField()
def get_metadata_download_url(self, instance: SAMLProvider) -> str:
"""Get metadata download URL"""
return (
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": instance.pk})
+ "?download"
)
class Meta: class Meta:
model = SAMLProvider model = SAMLProvider
@ -53,6 +69,7 @@ class SAMLProviderSerializer(ProviderSerializer):
"signing_kp", "signing_kp",
"verification_kp", "verification_kp",
"sp_binding", "sp_binding",
"metadata_download_url",
] ]
@ -75,7 +92,7 @@ class SAMLProviderImportSerializer(PassiveSerializer):
file = FileField() file = FileField()
class SAMLProviderViewSet(ModelViewSet): class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
"""SAMLProvider Viewset""" """SAMLProvider Viewset"""
queryset = SAMLProvider.objects.all() queryset = SAMLProvider.objects.all()
@ -166,7 +183,7 @@ class SAMLPropertyMappingSerializer(PropertyMappingSerializer):
] ]
class SAMLPropertyMappingViewSet(ModelViewSet): class SAMLPropertyMappingViewSet(UsedByMixin, ModelViewSet):
"""SAMLPropertyMapping Viewset""" """SAMLPropertyMapping Viewset"""
queryset = SAMLPropertyMapping.objects.all() queryset = SAMLPropertyMapping.objects.all()

View File

@ -120,7 +120,7 @@ class ServiceProviderMetadataParser:
) )
ctx.key = key ctx.key = key
ctx.verify(signature_node) ctx.verify(signature_node)
except xmlsec.VerificationError as exc: except xmlsec.Error as exc:
raise ValueError("Failed to verify Metadata signature") from exc raise ValueError("Failed to verify Metadata signature") from exc
def parse(self, raw_xml: str) -> ServiceProviderMetadata: def parse(self, raw_xml: str) -> ServiceProviderMetadata:

View File

@ -108,7 +108,7 @@ class AuthNRequestParser:
) )
ctx.key = key ctx.key = key
ctx.verify(signature_node) ctx.verify(signature_node)
except xmlsec.VerificationError as exc: except xmlsec.Error as exc:
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
return self._parse_xml(decoded_xml, relay_state) return self._parse_xml(decoded_xml, relay_state)
@ -160,7 +160,7 @@ class AuthNRequestParser:
sign_algorithm_transform, sign_algorithm_transform,
b64decode(signature), b64decode(signature),
) )
except xmlsec.VerificationError as exc: except xmlsec.Error as exc:
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
return self._parse_xml(decoded_xml, relay_state) return self._parse_xml(decoded_xml, relay_state)

View File

@ -2,12 +2,13 @@
from base64 import b64encode from base64 import b64encode
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.http.request import HttpRequest, QueryDict from django.http.request import QueryDict
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
from guardian.utils import get_anonymous_user from guardian.utils import get_anonymous_user
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.flows.tests.test_planner import dummy_get_response
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.assertion import AssertionProcessor from authentik.providers.saml.processors.assertion import AssertionProcessor
from authentik.providers.saml.processors.request_parser import AuthNRequestParser from authentik.providers.saml.processors.request_parser import AuthNRequestParser
@ -58,11 +59,6 @@ qNAZMq1DqpibfCBg
-----END CERTIFICATE-----""" -----END CERTIFICATE-----"""
def dummy_get_response(request: HttpRequest): # pragma: no cover
"""Dummy get_response for SessionMiddleware"""
return None
class TestAuthNRequest(TestCase): class TestAuthNRequest(TestCase):
"""Test AuthN Request generator and parser""" """Test AuthN Request generator and parser"""

View File

@ -7,6 +7,7 @@ from django.utils.translation import gettext as _
from django.views import View from django.views import View
from authentik.core.models import Token, TokenIntents from authentik.core.models import Token, TokenIntents
from authentik.stages.password import BACKEND_DJANGO
class UseTokenView(View): class UseTokenView(View):
@ -18,7 +19,7 @@ class UseTokenView(View):
if not tokens.exists(): if not tokens.exists():
raise Http404 raise Http404
token = tokens.first() token = tokens.first()
login(request, token.user, backend="django.contrib.auth.backends.ModelBackend") login(request, token.user, backend=BACKEND_DJANGO)
token.delete() token.delete()
messages.warning(request, _("Used recovery-link to authenticate.")) messages.warning(request, _("Used recovery-link to authenticate."))
return redirect("authentik_core:if-admin") return redirect("authentik_core:if-admin")

View File

@ -375,7 +375,11 @@ if _ERROR_REPORTING:
environment=CONFIG.y("error_reporting.environment", "customer"), environment=CONFIG.y("error_reporting.environment", "customer"),
send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False), send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False),
) )
set_tag("authentik.build_hash", os.environ.get(ENV_GIT_HASH_KEY, "tagged")) # Default to empty string as that is what docker has
build_hash = os.environ.get(ENV_GIT_HASH_KEY, "")
if build_hash == "":
build_hash = "tagged"
set_tag("authentik.build_hash", build_hash)
set_tag( set_tag(
"authentik.env", "kubernetes" if "KUBERNETES_PORT" in os.environ else "compose" "authentik.env", "kubernetes" if "KUBERNETES_PORT" in os.environ else "compose"
) )

View File

@ -10,6 +10,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.tasks import TaskSerializer from authentik.admin.api.tasks import TaskSerializer
from authentik.core.api.propertymappings import PropertyMappingSerializer from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.sources import SourceSerializer from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.events.monitored_tasks import TaskInfo from authentik.events.monitored_tasks import TaskInfo
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
@ -41,7 +42,7 @@ class LDAPSourceSerializer(SourceSerializer):
extra_kwargs = {"bind_password": {"write_only": True}} extra_kwargs = {"bind_password": {"write_only": True}}
class LDAPSourceViewSet(ModelViewSet): class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
"""LDAP Source Viewset""" """LDAP Source Viewset"""
queryset = LDAPSource.objects.all() queryset = LDAPSource.objects.all()
@ -75,7 +76,7 @@ class LDAPPropertyMappingSerializer(PropertyMappingSerializer):
] ]
class LDAPPropertyMappingViewSet(ModelViewSet): class LDAPPropertyMappingViewSet(UsedByMixin, ModelViewSet):
"""LDAP PropertyMapping Viewset""" """LDAP PropertyMapping Viewset"""
queryset = LDAPPropertyMapping.objects.all() queryset = LDAPPropertyMapping.objects.all()

View File

@ -9,6 +9,7 @@ from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.manager import MANAGER from authentik.sources.oauth.types.manager import MANAGER
@ -78,7 +79,7 @@ class OAuthSourceSerializer(SourceSerializer):
extra_kwargs = {"consumer_secret": {"write_only": True}} extra_kwargs = {"consumer_secret": {"write_only": True}}
class OAuthSourceViewSet(ModelViewSet): class OAuthSourceViewSet(UsedByMixin, ModelViewSet):
"""Source Viewset""" """Source Viewset"""
queryset = OAuthSource.objects.all() queryset = OAuthSource.objects.all()

View File

@ -6,6 +6,7 @@ from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.sources import SourceSerializer from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.sources.oauth.models import UserOAuthSourceConnection from authentik.sources.oauth.models import UserOAuthSourceConnection
@ -26,6 +27,7 @@ class UserOAuthSourceConnectionViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -14,6 +14,7 @@ from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.sources import SourceSerializer from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import RedirectChallenge from authentik.flows.challenge import RedirectChallenge
from authentik.flows.views import to_stage_response from authentik.flows.views import to_stage_response
@ -42,7 +43,7 @@ class PlexTokenRedeemSerializer(PassiveSerializer):
plex_token = CharField() plex_token = CharField()
class PlexSourceViewSet(ModelViewSet): class PlexSourceViewSet(UsedByMixin, ModelViewSet):
"""Plex source Viewset""" """Plex source Viewset"""
queryset = PlexSource.objects.all() queryset = PlexSource.objects.all()

View File

@ -7,6 +7,7 @@ from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.providers.saml.api import SAMLMetadataSerializer from authentik.providers.saml.api import SAMLMetadataSerializer
from authentik.sources.saml.models import SAMLSource from authentik.sources.saml.models import SAMLSource
from authentik.sources.saml.processors.metadata import MetadataProcessor from authentik.sources.saml.processors.metadata import MetadataProcessor
@ -33,7 +34,7 @@ class SAMLSourceSerializer(SourceSerializer):
] ]
class SAMLSourceViewSet(ModelViewSet): class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
"""SAMLSource Viewset""" """SAMLSource Viewset"""
queryset = SAMLSource.objects.all() queryset = SAMLSource.objects.all()

View File

@ -39,7 +39,7 @@ from authentik.sources.saml.processors.constants import (
from authentik.sources.saml.processors.request import SESSION_REQUEST_ID from authentik.sources.saml.processors.request import SESSION_REQUEST_ID
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
from authentik.stages.user_login.stage import DEFAULT_BACKEND from authentik.stages.user_login.stage import BACKEND_DJANGO
LOGGER = get_logger() LOGGER = get_logger()
if TYPE_CHECKING: if TYPE_CHECKING:
@ -141,7 +141,7 @@ class ResponseProcessor:
self._source.authentication_flow, self._source.authentication_flow,
**{ **{
PLAN_CONTEXT_PENDING_USER: user, PLAN_CONTEXT_PENDING_USER: user,
PLAN_CONTEXT_AUTHENTICATION_BACKEND: DEFAULT_BACKEND, PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
}, },
) )
@ -204,7 +204,7 @@ class ResponseProcessor:
self._source.authentication_flow, self._source.authentication_flow,
**{ **{
PLAN_CONTEXT_PENDING_USER: matching_users.first(), PLAN_CONTEXT_PENDING_USER: matching_users.first(),
PLAN_CONTEXT_AUTHENTICATION_BACKEND: DEFAULT_BACKEND, PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
PLAN_CONTEXT_REDIRECT: final_redirect, PLAN_CONTEXT_REDIRECT: final_redirect,
}, },
) )

View File

@ -2,7 +2,7 @@
from django.utils.timezone import now from django.utils.timezone import now
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import User from authentik.core.models import AuthenticatedSession, User
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
@ -31,11 +31,13 @@ def clean_temporary_users(self: MonitoredTask):
continue continue
source = sources.first() source = sources.first()
source_delta = timedelta_from_string(source.temporary_user_delete_after) source_delta = timedelta_from_string(source.temporary_user_delete_after)
if _now - user.last_login >= source_delta: if (
_now - user.last_login >= source_delta
and not AuthenticatedSession.objects.filter(user=user).exists()
):
LOGGER.debug( LOGGER.debug(
"User is expired and will be deleted.", user=user, delta=source_delta "User is expired and will be deleted.", user=user, delta=source_delta
) )
# TODO: Check if user is signed in anywhere?
user.delete() user.delete()
deleted_users += 1 deleted_users += 1
messages.append(f"Successfully deleted {deleted_users} users.") messages.append(f"Successfully deleted {deleted_users} users.")

View File

@ -12,6 +12,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_duo.stage import ( from authentik.stages.authenticator_duo.stage import (
@ -37,7 +38,7 @@ class AuthenticatorDuoStageSerializer(StageSerializer):
} }
class AuthenticatorDuoStageViewSet(ModelViewSet): class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
"""AuthenticatorDuoStage Viewset""" """AuthenticatorDuoStage Viewset"""
queryset = AuthenticatorDuoStage.objects.all() queryset = AuthenticatorDuoStage.objects.all()
@ -78,6 +79,7 @@ class DuoDeviceViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -1,44 +1,6 @@
# Generated by Django 3.1.1 on 2020-09-25 14:32 # Generated by Django 3.1.1 on 2020-09-25 14:32
from django.apps.registry import Apps
from django.db import migrations from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.flows.models import FlowDesignation
def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Flow = apps.get_model("authentik_flows", "Flow")
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
AuthenticatorDuoStage = apps.get_model(
"authentik_stages_authenticator_duo", "AuthenticatorDuoStage"
)
db_alias = schema_editor.connection.alias
flow, _ = Flow.objects.using(db_alias).update_or_create(
slug="default-authenticator-duo-setup",
designation=FlowDesignation.STAGE_CONFIGURATION,
defaults={
"name": "default-authenticator-duo-setup",
"title": "Setup Duo",
},
)
stage, _ = AuthenticatorDuoStage.objects.using(db_alias).update_or_create(
name="default-authenticator-duo-setup"
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow, stage=stage, defaults={"order": 0}
)
for stage in AuthenticatorDuoStage.objects.using(db_alias).filter(
configure_flow=None
):
stage.configure_flow = flow
stage.save()
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -50,6 +12,4 @@ class Migration(migrations.Migration):
), ),
] ]
operations = [ operations = []
migrations.RunPython(create_default_setup_flow),
]

View File

@ -8,6 +8,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
@ -21,7 +22,7 @@ class AuthenticatorStaticStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + ["configure_flow", "token_count"] fields = StageSerializer.Meta.fields + ["configure_flow", "token_count"]
class AuthenticatorStaticStageViewSet(ModelViewSet): class AuthenticatorStaticStageViewSet(UsedByMixin, ModelViewSet):
"""AuthenticatorStaticStage Viewset""" """AuthenticatorStaticStage Viewset"""
queryset = AuthenticatorStaticStage.objects.all() queryset = AuthenticatorStaticStage.objects.all()
@ -52,6 +53,7 @@ class StaticDeviceViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -8,6 +8,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
@ -21,7 +22,7 @@ class AuthenticatorTOTPStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + ["configure_flow", "digits"] fields = StageSerializer.Meta.fields + ["configure_flow", "digits"]
class AuthenticatorTOTPStageViewSet(ModelViewSet): class AuthenticatorTOTPStageViewSet(UsedByMixin, ModelViewSet):
"""AuthenticatorTOTPStage Viewset""" """AuthenticatorTOTPStage Viewset"""
queryset = AuthenticatorTOTPStage.objects.all() queryset = AuthenticatorTOTPStage.objects.all()
@ -45,6 +46,7 @@ class TOTPDeviceViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -3,4 +3,4 @@
INSTALLED_APPS = [ INSTALLED_APPS = [
"django_otp.plugins.otp_totp", "django_otp.plugins.otp_totp",
] ]
OTP_TOTP_ISSUER = "authentik" OTP_TOTP_ISSUER = "__to_replace__"

View File

@ -1,6 +1,7 @@
"""TOTP Setup stage""" """TOTP Setup stage"""
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict from django.http.request import QueryDict
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_totp.models import TOTPDevice
from rest_framework.fields import CharField, IntegerField from rest_framework.fields import CharField, IntegerField
@ -16,6 +17,7 @@ from authentik.flows.challenge import (
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
from authentik.stages.authenticator_totp.settings import OTP_TOTP_ISSUER
LOGGER = get_logger() LOGGER = get_logger()
SESSION_TOTP_DEVICE = "totp_device" SESSION_TOTP_DEVICE = "totp_device"
@ -54,7 +56,9 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
return AuthenticatorTOTPChallenge( return AuthenticatorTOTPChallenge(
data={ data={
"type": ChallengeTypes.NATIVE.value, "type": ChallengeTypes.NATIVE.value,
"config_url": device.config_url, "config_url": device.config_url.replace(
OTP_TOTP_ISSUER, slugify(self.request.tenant.branding_title)
),
} }
) )

View File

@ -2,6 +2,7 @@
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
from authentik.flows.models import NotConfiguredAction from authentik.flows.models import NotConfiguredAction
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
@ -32,7 +33,7 @@ class AuthenticatorValidateStageSerializer(StageSerializer):
] ]
class AuthenticatorValidateStageViewSet(ModelViewSet): class AuthenticatorValidateStageViewSet(UsedByMixin, ModelViewSet):
"""AuthenticatorValidateStage Viewset""" """AuthenticatorValidateStage Viewset"""
queryset = AuthenticatorValidateStage.objects.all() queryset = AuthenticatorValidateStage.objects.all()

View File

@ -91,6 +91,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
"""Get a list of all device challenges applicable for the current stage""" """Get a list of all device challenges applicable for the current stage"""
challenges = [] challenges = []
user_devices = devices_for_user(self.get_pending_user()) user_devices = devices_for_user(self.get_pending_user())
LOGGER.debug("Got devices for user", devices=user_devices)
# static and totp are only shown once # static and totp are only shown once
# since their challenges are device-independant # since their challenges are device-independant
@ -101,6 +102,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
for device in user_devices: for device in user_devices:
device_class = device.__class__.__name__.lower().replace("device", "") device_class = device.__class__.__name__.lower().replace("device", "")
if device_class not in stage.device_classes: if device_class not in stage.device_classes:
LOGGER.debug("device class not allowed", device_class=device_class)
continue continue
# Ensure only classes in PER_DEVICE_CLASSES are returned per device # Ensure only classes in PER_DEVICE_CLASSES are returned per device
# otherwise only return a single challenge # otherwise only return a single challenge
@ -108,15 +110,16 @@ class AuthenticatorValidateStageView(ChallengeStageView):
continue continue
if device_class not in seen_classes: if device_class not in seen_classes:
seen_classes.append(device_class) seen_classes.append(device_class)
challenges.append( challenge = DeviceChallenge(
DeviceChallenge(
data={ data={
"device_class": device_class, "device_class": device_class,
"device_uid": device.pk, "device_uid": device.pk,
"challenge": get_challenge_for_device(self.request, device), "challenge": get_challenge_for_device(self.request, device),
} }
).initial_data
) )
challenge.is_valid()
challenges.append(challenge.data)
LOGGER.debug("adding challenge for device", challenge=challenge)
return challenges return challenges
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:

View File

@ -0,0 +1,127 @@
"""Test validator stage"""
from unittest.mock import MagicMock, patch
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase
from django.test.client import RequestFactory
from django_otp.plugins.otp_totp.models import TOTPDevice
from rest_framework.exceptions import ValidationError
from authentik.core.models import User
from authentik.flows.models import NotConfiguredAction
from authentik.flows.tests.test_planner import dummy_get_response
from authentik.providers.oauth2.generators import (
generate_client_id,
generate_client_secret,
)
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_validate.api import (
AuthenticatorValidateStageSerializer,
)
from authentik.stages.authenticator_validate.challenge import (
get_challenge_for_device,
validate_challenge_code,
validate_challenge_duo,
validate_challenge_webauthn,
)
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
class AuthenticatorValidateStageTests(TestCase):
"""Test validator stage"""
def setUp(self) -> None:
self.user = User.objects.get(username="akadmin")
self.request_factory = RequestFactory()
def test_stage_validation(self):
"""Test serializer validation"""
self.client.force_login(self.user)
serializer = AuthenticatorValidateStageSerializer(
data={"name": "foo", "not_configured_action": NotConfiguredAction.CONFIGURE}
)
self.assertFalse(serializer.is_valid())
self.assertIn("not_configured_action", serializer.errors)
def test_device_challenge_totp(self):
"""Test device challenge"""
request = self.request_factory.get("/")
totp_device = TOTPDevice.objects.create(
user=self.user, confirmed=True, digits=6
)
self.assertEqual(get_challenge_for_device(request, totp_device), {})
with self.assertRaises(ValidationError):
validate_challenge_code("1234", request, self.user)
def test_device_challenge_webauthn(self):
"""Test webauthn"""
request = self.request_factory.get("/")
request.user = self.user
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
webauthn_device = WebAuthnDevice.objects.create(
user=self.user,
public_key="qwerqwerqre",
credential_id="foobarbaz",
sign_count=0,
rp_id="foo",
)
challenge = get_challenge_for_device(request, webauthn_device)
del challenge["challenge"]
self.assertEqual(
challenge,
{
"allowCredentials": [
{
"id": "foobarbaz",
"transports": ["usb", "nfc", "ble", "internal"],
"type": "public-key",
}
],
"rpId": "foo",
"timeout": 60000,
"userVerification": "discouraged",
},
)
with self.assertRaises(ValidationError):
validate_challenge_webauthn({}, request, self.user)
def test_device_challenge_duo(self):
"""Test duo"""
request = self.request_factory.get("/")
stage = AuthenticatorDuoStage.objects.create(
name="test",
client_id=generate_client_id(),
client_secret=generate_client_secret(),
api_hostname="",
)
duo_device = DuoDevice.objects.create(
user=self.user,
stage=stage,
)
duo_mock = MagicMock(
auth=MagicMock(
return_value={
"result": "allow",
"status": "allow",
"status_msg": "Success. Logging you in...",
}
)
)
failed_duo_mock = MagicMock(auth=MagicMock(return_value={"result": "deny"}))
with patch(
"authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.client",
duo_mock,
):
self.assertEqual(
duo_device.pk, validate_challenge_duo(duo_device.pk, request, self.user)
)
with patch(
"authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.client",
failed_duo_mock,
):
with self.assertRaises(ValidationError):
validate_challenge_duo(duo_device.pk, request, self.user)

View File

@ -7,6 +7,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_webauthn.models import ( from authentik.stages.authenticator_webauthn.models import (
AuthenticateWebAuthnStage, AuthenticateWebAuthnStage,
@ -23,7 +24,7 @@ class AuthenticateWebAuthnStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + ["configure_flow"] fields = StageSerializer.Meta.fields + ["configure_flow"]
class AuthenticateWebAuthnStageViewSet(ModelViewSet): class AuthenticateWebAuthnStageViewSet(UsedByMixin, ModelViewSet):
"""AuthenticateWebAuthnStage Viewset""" """AuthenticateWebAuthnStage Viewset"""
queryset = AuthenticateWebAuthnStage.objects.all() queryset = AuthenticateWebAuthnStage.objects.all()
@ -44,6 +45,7 @@ class WebAuthnDeviceViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -28,8 +28,6 @@ from authentik.stages.authenticator_webauthn.utils import (
get_rp_id, get_rp_id,
) )
RP_NAME = "authentik"
LOGGER = get_logger() LOGGER = get_logger()
SESSION_KEY_WEBAUTHN_AUTHENTICATED = ( SESSION_KEY_WEBAUTHN_AUTHENTICATED = (
@ -119,7 +117,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
user = self.get_pending_user() user = self.get_pending_user()
make_credential_options = WebAuthnMakeCredentialOptions( make_credential_options = WebAuthnMakeCredentialOptions(
challenge, challenge,
RP_NAME, self.request.tenant.branding_title,
get_rp_id(self.request), get_rp_id(self.request),
user.uid, user.uid,
user.username, user.username,

View File

@ -1,6 +1,7 @@
"""CaptchaStage API Views""" """CaptchaStage API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
from authentik.stages.captcha.models import CaptchaStage from authentik.stages.captcha.models import CaptchaStage
@ -15,7 +16,7 @@ class CaptchaStageSerializer(StageSerializer):
extra_kwargs = {"private_key": {"write_only": True}} extra_kwargs = {"private_key": {"write_only": True}}
class CaptchaStageViewSet(ModelViewSet): class CaptchaStageViewSet(UsedByMixin, ModelViewSet):
"""CaptchaStage Viewset""" """CaptchaStage Viewset"""
queryset = CaptchaStage.objects.all() queryset = CaptchaStage.objects.all()

View File

@ -6,6 +6,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.applications import ApplicationSerializer from authentik.core.api.applications import ApplicationSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer from authentik.core.api.users import UserSerializer
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
from authentik.stages.consent.models import ConsentStage, UserConsent from authentik.stages.consent.models import ConsentStage, UserConsent
@ -20,7 +21,7 @@ class ConsentStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + ["mode", "consent_expire_in"] fields = StageSerializer.Meta.fields + ["mode", "consent_expire_in"]
class ConsentStageViewSet(ModelViewSet): class ConsentStageViewSet(UsedByMixin, ModelViewSet):
"""ConsentStage Viewset""" """ConsentStage Viewset"""
queryset = ConsentStage.objects.all() queryset = ConsentStage.objects.all()
@ -42,6 +43,7 @@ class UserConsentSerializer(StageSerializer):
class UserConsentViewSet( class UserConsentViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet, GenericViewSet,
): ):

View File

@ -1,6 +1,7 @@
"""deny Stage API Views""" """deny Stage API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
from authentik.stages.deny.models import DenyStage from authentik.stages.deny.models import DenyStage
@ -14,7 +15,7 @@ class DenyStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields fields = StageSerializer.Meta.fields
class DenyStageViewSet(ModelViewSet): class DenyStageViewSet(UsedByMixin, ModelViewSet):
"""DenyStage Viewset""" """DenyStage Viewset"""
queryset = DenyStage.objects.all() queryset = DenyStage.objects.all()

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