Compare commits

..

188 Commits

Author SHA1 Message Date
31a58e2c25 release: 2021.6.2 2021-06-22 23:35:10 +02:00
229715acb2 ci: fix push as stable
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-22 23:33:36 +02:00
2a670afd02 Break down Sources into individual sections in Docs (#1052)
* Create index.mdx

Add Wekan example

* updated to include wekan entry

* Update and rename website/docs/sources.md to website/docs/sources/index.md

Break Sources into individual pages.

* Update and rename website/docs/sources/index.md to website/docs/sources/ldap/index.md

* Create index.md

* Update index.md

* Update index.md

* Create index.md

* Create index.md

* Create index.md

* Update index.md

* Update index.md

* Update index.md

* Create index.md

* discord images

* spacing

* Added discord

* discord changes

* Added sources breakdown to the sidebar

* Fixed the saml title

* Added github examples

* fixed formatting

* Changed file path, updated sidebar, added google.

* fixed a spelling mistake

* Cleaned up formatting

* Fixed Notes
2021-06-22 21:46:44 +02:00
b69248dd55 stages/authenticator_validate: fix error when using not_configured_action=configure
closes #1048

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-22 20:08:58 +02:00
5ff5edf769 outposts: improve logging
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-22 18:51:02 +02:00
939889e0ec tenants: fix footer_links for moved config
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-22 15:48:17 +02:00
19ae6585dc lib: add tests for config loader
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-22 13:12:07 +02:00
a81c847392 website/docs: fix call to group_attributes for nextcloud
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-22 13:00:48 +02:00
c6ede78fba core: add support for custom urls for avatars
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-22 12:25:24 +02:00
cea1289186 website/docs: add instruction for local.env.yml for frontend dev
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-22 12:06:55 +02:00
c297f28552 build(deps): bump @typescript-eslint/parser in /web (#1060) 2021-06-22 08:55:04 +02:00
35b25bd76e build(deps): bump @sentry/browser from 6.7.1 to 6.7.2 in /web (#1061) 2021-06-22 08:54:56 +02:00
64d7610b13 build(deps): bump boto3 from 1.17.97 to 1.17.98 (#1065) 2021-06-22 08:11:27 +02:00
2c8fcff832 build(deps): bump codemirror from 5.61.1 to 5.62.0 in /web (#1058) 2021-06-22 08:11:11 +02:00
054e76d02a build(deps): bump @babel/preset-env from 7.14.5 to 7.14.7 in /web (#1059) 2021-06-22 08:10:56 +02:00
80fa132dd9 build(deps): bump @typescript-eslint/eslint-plugin in /web (#1062) 2021-06-22 08:10:39 +02:00
4c59c3abef build(deps): bump @sentry/tracing from 6.7.1 to 6.7.2 in /web (#1063) 2021-06-22 08:10:27 +02:00
22d319c0e7 build(deps): bump rollup from 2.52.1 to 2.52.2 in /web (#1064) 2021-06-22 08:09:44 +02:00
89edd77484 website/docs: use beta images for dev setup
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 22:57:18 +02:00
04e52d8ba6 web/admin: handle elements in slot=form not being forms
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 22:48:47 +02:00
9b5e3921cb providers/saml: better handle decoding errors
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 22:48:34 +02:00
2bbad64dc3 website/docs: add developer docs for frontend-only
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 21:25:56 +02:00
f6026fdb13 root: allow loading local /static files without debug flag
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 21:21:35 +02:00
49def45ca3 root: remove old traefik labels
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 21:04:59 +02:00
a4856969f4 outposts: fix port and inner_port being mixed on docker controller
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 19:19:06 +02:00
2aa7266688 crypto: fix linting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 16:24:03 +02:00
25817cae6b ci: always run full test, send sourcemaps to sentry
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 16:12:14 +02:00
5383ae2c19 ci: re-tag latest images on stable build instead of building again
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 16:11:30 +02:00
c0c246edab crypto: catch error when loading private key
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 15:57:48 +02:00
831b32c279 core: fix PropertyMapping's globals not matching Expression policy
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 15:54:43 +02:00
70ccc63702 core: remove default flow background from default css, set static in base_full and dynamically in if/flow
closes #1056

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 10:37:34 +02:00
de954250e5 root: make general cache timeouts configurable
closes #974

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 10:18:49 +02:00
f268bd4c69 policies: make policy result cache timeout configurable
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 10:17:58 +02:00
57a48b6350 flows: make flow plan cache timeout configurable
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 10:17:11 +02:00
9aac114115 root: save temporary database dump in /tmp
closes #1055

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-21 09:58:19 +02:00
66e3cbdc46 build(deps): bump eslint from 7.28.0 to 7.29.0 in /web (#1053) 2021-06-21 08:49:06 +02:00
2d76d23f7b root: add pr_wanted exemption to stale bot
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-20 17:27:54 +02:00
4327b35bc3 tenants: fix tenant not being queried correctly when using accessing over a child domain
closes #1044

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-20 14:39:21 +02:00
f7047df40e policies: don't use policy cache when checking application access
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-20 13:30:07 +02:00
ef77a4b64e tests/e2e: fix provider test image
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-19 22:11:09 +02:00
5d7d21076f tests/integration: fix expected image names
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-19 20:22:20 +02:00
ede072889e core: deepmerge user.group_attributes, use group_attributes for user settings
closes #1051

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-19 19:52:55 +02:00
9cb7e6c606 root: set outposts.docker_image_base to gh-master for tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-19 15:49:49 +02:00
e7d36c095d web/admin: sort inputs on authenticator validation stage form
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-19 15:35:39 +02:00
b88eb430c1 outposts/proxy: fix additionalHeaders not being set
closes #1050

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-19 15:24:51 +02:00
641872a33a web/admin: fix tenant's default flag not being saved
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1044
2021-06-19 12:42:29 +02:00
405c690193 tests/e2e: test additionalHeaders with proxy
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-19 12:40:24 +02:00
932cf48d2b website/docs: remove old branding settings
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-18 09:10:19 +02:00
402819107d build(deps): bump boto3 from 1.17.96 to 1.17.97 (#1046) 2021-06-18 07:24:02 +02:00
41f135126b build(deps): bump typescript from 4.3.3 to 4.3.4 in /web (#1045) 2021-06-18 07:23:49 +02:00
591a339302 build(deps): bump celery from 5.1.0 to 5.1.1 (#1047) 2021-06-18 07:23:41 +02:00
35f2c5d96a website/docs: add release notes for 2021.6
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-17 22:52:39 +02:00
fe6963c428 release: 2021.6.1 2021-06-17 22:14:52 +02:00
19cac4bf43 providers/saml: fix error when getting transient user identifier
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-17 13:52:10 +02:00
4ca564490e providers/saml: add support for NameID type unspecified
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-17 12:45:53 +02:00
fcb795c273 providers/saml: fix NameIDPolicy not being parsed correctly, improve error handling
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-17 12:22:40 +02:00
14c70b3e4a build(deps): bump rollup from 2.52.0 to 2.52.1 in /web (#1039) 2021-06-17 08:53:11 +02:00
ac880c28d7 build(deps): bump rollup from 2.51.2 to 2.52.0 in /web (#1033) 2021-06-17 08:51:31 +02:00
f3c6b9a4f6 build(deps): bump postcss from 8.3.4 to 8.3.5 in /website (#1034) 2021-06-17 08:51:22 +02:00
cba0cf0d76 build(deps): bump @lingui/core from 3.10.3 to 3.10.4 in /web (#1035) 2021-06-17 08:51:11 +02:00
73b67cf0f0 build(deps): bump typescript from 4.3.2 to 4.3.3 in /web (#1036) 2021-06-17 08:51:00 +02:00
23a8052cc8 build(deps): bump boto3 from 1.17.95 to 1.17.96 (#1037) 2021-06-17 08:50:52 +02:00
57c49c3865 build(deps): bump psycopg2-binary from 2.8.6 to 2.9.1 (#1038) 2021-06-17 08:50:43 +02:00
cbea51ae5b stages/authenticator_duo: make Duo-admin viewset writeable
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-16 23:17:26 +02:00
8962081d92 website/docs: add wekan (#1032)
* Create index.mdx

Add Wekan example

* updated to include wekan entry
2021-06-16 23:08:58 +02:00
e743f13f81 recovery: fix error when creating multiple keys for the same user
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-16 23:04:35 +02:00
b20a8b7c17 stages/authenticator_duo: fix error when enrolling an existing user
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-16 23:04:24 +02:00
b53c94d76a flows: fix error when stage has incorrect type
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-16 22:52:00 +02:00
d4419d66c1 core: fix error when creating AuthenticatedSession without key
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-16 22:51:48 +02:00
79044368d2 core: fix error getting stages when enrollment flow isn't set
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-16 22:45:42 +02:00
426686957d website/docs: remove migrate command
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-16 22:43:43 +02:00
28cb803fd9 website/docs: Add a note about Protocol Overwrite (#1031)
Added a note in the Nextcloud section for Protocol overwrite when behind a reverse proxy
2021-06-16 19:38:34 +02:00
85c3a36b62 website: clear up comparison
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-16 14:54:44 +02:00
9ba8a715b1 build(deps): bump @sentry/tracing from 6.7.0 to 6.7.1 in /web (#1026) 2021-06-16 09:26:32 +02:00
358750f66e build(deps): bump drf-spectacular from 0.17.1 to 0.17.2 (#1028) 2021-06-16 08:47:05 +02:00
b9918529b8 build(deps): bump @sentry/browser from 6.7.0 to 6.7.1 in /web (#1027) 2021-06-16 08:46:40 +02:00
a5673b4ec8 build(deps): bump boto3 from 1.17.94 to 1.17.95 (#1029) 2021-06-16 08:46:11 +02:00
d9287d0c0e Merge branch 'next' 2021-06-15 23:43:44 +02:00
d9c2b64116 root: update schema
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-15 23:38:03 +02:00
2b150d3077 website/docs: add changelog for release candidates
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-15 22:19:45 +02:00
dec7a9cfb9 website/docs: add docs for flow executor
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-15 22:14:23 +02:00
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
277 changed files with 6936 additions and 2503 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2021.6.1-rc1 current_version = 2021.6.2
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

1
.github/stale.yml vendored
View File

@ -6,6 +6,7 @@ daysUntilClose: 7
exemptLabels: exemptLabels:
- pinned - pinned
- security - security
- pr_wanted
# Comment to post when marking an issue as stale. Set to `false` to disable # Comment to post when marking an issue as stale. Set to `false` to disable
markComment: > markComment: >
This issue has been automatically marked as stale because it has not had This issue has been automatically marked as stale because it has not had

View File

@ -33,12 +33,21 @@ 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.2,
beryju/authentik:latest, beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.6.1-rc1, ghcr.io/goauthentik/server:2021.6.2,
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)
if: ${{ github.event_name == 'release' && !contains('2021.6.2', 'rc') }}
run: |
docker pull beryju/authentik:latest
docker tag beryju/authentik:latest beryju/authentik:stable
docker push beryju/authentik:stable
docker pull ghcr.io/goauthentik/server:latest
docker tag ghcr.io/goauthentik/server:latest ghcr.io/goauthentik/server:stable
docker push ghcr.io/goauthentik/server:stable
build-proxy: build-proxy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -66,12 +75,21 @@ 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.2,
beryju/authentik-proxy:latest, beryju/authentik-proxy:latest,
ghcr.io/goauthentik/proxy:2021.6.1-rc1, ghcr.io/goauthentik/proxy:2021.6.2,
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)
if: ${{ github.event_name == 'release' && !contains('2021.6.2', 'rc') }}
run: |
docker pull beryju/authentik-proxy:latest
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
docker push beryju/authentik-proxy:stable
docker pull ghcr.io/goauthentik/proxy:latest
docker tag ghcr.io/goauthentik/proxy:latest ghcr.io/goauthentik/proxy:stable
docker push ghcr.io/goauthentik/proxy:stable
build-ldap: build-ldap:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -99,14 +117,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.2,
beryju/authentik-ldap:latest, beryju/authentik-ldap:latest,
ghcr.io/goauthentik/ldap:2021.6.1-rc1, ghcr.io/goauthentik/ldap:2021.6.2,
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)
if: ${{ github.event_name == 'release' && !contains('2021.6.2', 'rc') }}
run: |
docker pull beryju/authentik-ldap:latest
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
docker push beryju/authentik-ldap:stable
docker pull ghcr.io/goauthentik/ldap:latest
docker tag ghcr.io/goauthentik/ldap:latest ghcr.io/goauthentik/ldap:stable
docker push ghcr.io/goauthentik/ldap:stable
test-release: test-release:
if: ${{ github.event_name == 'release' }}
needs: needs:
- build-server - build-server
- build-proxy - build-proxy
@ -122,7 +148,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:
@ -130,13 +156,26 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.5
with:
node-version: 12.x
- name: Build web api client and web ui
run: |
export NODE_ENV=production
make gen-web
cd web
npm i
npm run build
- name: Create a Sentry.io release - name: Create a Sentry.io release
uses: getsentry/action-release@v1 uses: getsentry/action-release@v1
if: ${{ github.event_name == 'release' }}
env: env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: beryjuorg SENTRY_ORG: beryjuorg
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.2
environment: beryjuorg-prod environment: beryjuorg-prod
sourcemaps: './web/dist'

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

View File

@ -46,6 +46,7 @@ webauthn = "*"
xmlsec = "*" xmlsec = "*"
duo-client = "*" duo-client = "*"
ua-parser = "*" ua-parser = "*"
deepmerge = "*"
[requires] [requires]
python_version = "3.9" python_version = "3.9"

224
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "4fa1ad681762c867a95410074f31ac5d00119e187e0f38982cd59fdf301cccf5" "sha256": "f90d9fb4713eaf9c5ffe6a3858e64843670f79ab5007e7debf914c1f094c8d63"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -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:2c2f70608934b03f9c08f4cd185de223b5abd18245dd4d4800e1fbc2a2523e31",
"sha256:bbf727d770a9844834bfbf3f811db1d3438320897f67cfb21cdca5bb8fc23c13" "sha256:fccfa81cda69bb2317ed97e7149d7d84d19e6ec3bfbe3f721139e7ac0c407c73"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.17.90" "version": "==1.17.98"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:6ae4ff3405cc4fc69ff3673a8dd234bf869aa556ae1e0da050d7f2aa3c3edab6", "sha256:b2a49de4ee04b690142c8e7240f0f5758e3f7673dd39cf398efe893bf5e11c3f",
"sha256:b301810c4bd6cab1b6eaf6bfd9f25abb27959b586c2e1689bbce035b3fb8ae66" "sha256:b955b23fe2fbdbbc8e66f37fe2970de6b5d8169f940b200bcf434751709d38f6"
], ],
"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.98"
}, },
"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,15 +160,16 @@
"sha256:ce6219986385778b1ab7f9b542f160bb4d3558f52975e914a27b774e47016fb7", "sha256:ce6219986385778b1ab7f9b542f160bb4d3558f52975e914a27b774e47016fb7",
"sha256:d562b2773e14ee1d65ea5b85351a83a64d4f3fd011bc2b4c70a6e813e78203ce" "sha256:d562b2773e14ee1d65ea5b85351a83a64d4f3fd011bc2b4c70a6e813e78203ce"
], ],
"markers": "python_version >= '3.6'",
"version": "==5.4.0" "version": "==5.4.0"
}, },
"celery": { "celery": {
"hashes": [ "hashes": [
"sha256:1329de1edeaf734ef859e630cb42df2c116d53e59d2f46433b13aed196e85620", "sha256:54436cd97b031bf2e08064223240e2a83d601d9414bcb1b702f94c6c33c29485",
"sha256:65f061c04578cf189cd7352c192e1a79fdeb370b916bff792bcc769560e81184" "sha256:b5399d76cf70d5cfac3ec993f8796ec1aa90d4cef55972295751f384758a80d7"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.1.0" "version": "==5.1.1"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
@ -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,8 +321,17 @@
"sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f", "sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f",
"sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393" "sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"
], ],
"markers": "python_version >= '3.6'",
"version": "==3.0.2" "version": "==3.0.2"
}, },
"deepmerge": {
"hashes": [
"sha256:87166dbe9ba1a3348a45c9d4ada6778f518d41afc0b85aa017ea3041facc3f9c",
"sha256:f6fd7f1293c535fb599e197e750dbe8674503c5d2a89759b3c72a3c46746d4fd"
],
"index": "pypi",
"version": "==0.3.0"
},
"defusedxml": { "defusedxml": {
"hashes": [ "hashes": [
"sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
@ -414,11 +434,11 @@
}, },
"drf-spectacular": { "drf-spectacular": {
"hashes": [ "hashes": [
"sha256:4d35e890b8139e1c056588c5529a2f2066615635482563f0840b96d3b879d7d2", "sha256:6ffbfde7d96a4a2febd19182cc405217e1e86a50280fc739402291c93d1a32b7",
"sha256:f552476dfde647963c21615249672e7f4f9ece3788036b5ee5c6cc5ad50748ab" "sha256:77593024bb899f69227abedcf87def7851a11c9978f781aa4b385a10f67a38b7"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.17.0" "version": "==0.17.2"
}, },
"duo-client": { "duo-client": {
"hashes": [ "hashes": [
@ -440,6 +460,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 +473,11 @@
}, },
"google-auth": { "google-auth": {
"hashes": [ "hashes": [
"sha256:9b235dbc876e49454cbedc52ae0abd540ef705ebccdf4fbe93553bb13f26b1a4", "sha256:b3a67fa9ba5b768861dacf374c2135eb09fa14a0e40c851c3b8ea7abe6fc8fef",
"sha256:eb017521276a75492282c6ca4b718f26de112ed3bcbeaeeb02c1b82de425f909" "sha256:e34e5f5de5610b202f9b40ebd9f8b27571d5c5537db9afed3a72b2db5a345039"
], ],
"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.32.0"
}, },
"gunicorn": { "gunicorn": {
"hashes": [ "hashes": [
@ -470,6 +492,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 +539,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 +588,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 +596,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 +611,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 +625,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 +689,7 @@
"hashes": [ "hashes": [
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc" "sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
], ],
"markers": "python_version >= '3.6'",
"version": "==2.0.3" "version": "==2.0.3"
}, },
"msgpack": { "msgpack": {
@ -734,6 +765,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 +773,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,67 +789,85 @@
"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": {
"hashes": [ "hashes": [
"sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04", "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f",
"sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc" "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"
], ],
"version": "==3.0.18" "markers": "python_full_version >= '3.6.1'",
"version": "==3.0.19"
}, },
"psycopg2-binary": { "psycopg2-binary": {
"hashes": [ "hashes": [
"sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", "sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975",
"sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", "sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd",
"sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", "sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616",
"sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6", "sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2",
"sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", "sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90",
"sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", "sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a",
"sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", "sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e",
"sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056", "sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d",
"sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", "sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed",
"sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", "sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a",
"sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", "sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140",
"sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", "sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32",
"sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", "sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31",
"sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", "sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a",
"sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", "sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917",
"sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", "sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf",
"sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", "sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7",
"sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", "sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0",
"sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", "sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72",
"sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", "sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698",
"sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773",
"sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", "sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68",
"sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76",
"sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", "sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4",
"sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", "sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f",
"sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", "sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34",
"sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", "sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce",
"sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", "sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a",
"sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", "sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e"
"sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da",
"sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a",
"sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c",
"sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb",
"sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4",
"sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.8.6" "version": "==2.9.1"
}, },
"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 +876,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 +920,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 +943,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,14 +958,15 @@
"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": {
"hashes": [ "hashes": [
"sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544", "sha256:dd8fe852847f4fbfadabf6183ddd4c824a9651f02d51714fa075c95561959c7d",
"sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f" "sha256:effaac3c1e58d89b3ccb4d04a40dc7ad6e0275fda25fd75ae9d323e2465e202d"
], ],
"version": "==0.17.1" "version": "==0.18.0"
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
@ -959,6 +1015,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 +1023,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 +1071,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 +1079,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 +1135,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 +1159,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 +1204,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 +1231,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 +1332,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 +1389,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 +1406,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 +1414,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 +1453,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 +1461,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,14 +1535,16 @@
"sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0",
"sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"
], ],
"markers": "python_version >= '3.4'",
"version": "==4.0.7" "version": "==4.0.7"
}, },
"gitpython": { "gitpython": {
"hashes": [ "hashes": [
"sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135", "sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b",
"sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e" "sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8"
], ],
"version": "==3.1.17" "markers": "python_version >= '3.6'",
"version": "==3.1.18"
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
@ -1489,10 +1562,11 @@
}, },
"isort": { "isort": {
"hashes": [ "hashes": [
"sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6", "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56",
"sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d" "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"
], ],
"version": "==5.8.0" "markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
"version": "==5.9.1"
}, },
"lazy-object-proxy": { "lazy-object-proxy": {
"hashes": [ "hashes": [
@ -1519,6 +1593,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 +1630,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 +1638,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 +1646,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 +1677,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 +1782,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 +1806,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 +1814,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 +1822,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 +1830,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.2"
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

@ -3,23 +3,33 @@ from traceback import format_tb
from typing import Optional from typing import Optional
from django.http import HttpRequest from django.http import HttpRequest
from guardian.utils import get_anonymous_user
from authentik.core.models import User from authentik.core.models import PropertyMapping, User
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.lib.expression.evaluator import BaseEvaluator from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.policies.types import PolicyRequest
class PropertyMappingEvaluator(BaseEvaluator): class PropertyMappingEvaluator(BaseEvaluator):
"""Custom Evalautor that adds some different context variables.""" """Custom Evalautor that adds some different context variables."""
def set_context( def set_context(
self, user: Optional[User], request: Optional[HttpRequest], **kwargs self,
user: Optional[User],
request: Optional[HttpRequest],
mapping: PropertyMapping,
**kwargs,
): ):
"""Update context with context from PropertyMapping's evaluate""" """Update context with context from PropertyMapping's evaluate"""
req = PolicyRequest(user=get_anonymous_user())
req.obj = mapping
if user: if user:
req.user = user
self._context["user"] = user self._context["user"] = user
if request: if request:
self._context["request"] = request req.http_request = request
self._context["request"] = req
self._context.update(**kwargs) self._context.update(**kwargs)
def handle_error(self, exc: Exception, expression_source: str): def handle_error(self, exc: Exception, expression_source: str):
@ -30,9 +40,8 @@ class PropertyMappingEvaluator(BaseEvaluator):
expression=expression_source, expression=expression_source,
message=error_string, message=error_string,
) )
if "user" in self._context:
event.set_user(self._context["user"])
if "request" in self._context: if "request" in self._context:
event.from_http(self._context["request"]) req: PolicyRequest = self._context["request"]
event.from_http(req.http_request, req.user)
return return
event.save() event.save()

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,8 @@ 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 deepmerge import always_merger
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 +43,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)
@ -110,8 +115,8 @@ class User(GuardianUserMixin, AbstractUser):
including the users attributes""" including the users attributes"""
final_attributes = {} final_attributes = {}
for group in self.ak_groups.all().order_by("name"): for group in self.ak_groups.all().order_by("name"):
final_attributes.update(group.attributes) always_merger.merge(final_attributes, group.attributes)
final_attributes.update(self.attributes) always_merger.merge(final_attributes, self.attributes)
return final_attributes return final_attributes
@cached_property @cached_property
@ -138,21 +143,25 @@ class User(GuardianUserMixin, AbstractUser):
@property @property
def avatar(self) -> str: def avatar(self) -> str:
"""Get avatar, depending on authentik.avatar setting""" """Get avatar, depending on authentik.avatar setting"""
mode = CONFIG.raw.get("authentik").get("avatars") mode: str = CONFIG.y("avatars", "none")
if mode == "none": if mode == "none":
return DEFAULT_AVATAR return DEFAULT_AVATAR
# gravatar uses md5 for their URLs, so md5 can't be avoided
mail_hash = md5(self.email.encode("utf-8")).hexdigest() # nosec
if mode == "gravatar": if mode == "gravatar":
parameters = [ parameters = [
("s", "158"), ("s", "158"),
("r", "g"), ("r", "g"),
] ]
# gravatar uses md5 for their URLs, so md5 can't be avoided
mail_hash = md5(self.email.encode("utf-8")).hexdigest() # nosec
gravatar_url = ( gravatar_url = (
f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}" f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}"
) )
return escape(gravatar_url) return escape(gravatar_url)
raise ValueError(f"Invalid avatar mode {mode}") return mode % {
"username": self.username,
"mail_hash": mail_hash,
"upn": self.attributes.get("upn", ""),
}
class Meta: class Meta:
@ -456,7 +465,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
from authentik.core.expression import PropertyMappingEvaluator from authentik.core.expression import PropertyMappingEvaluator
evaluator = PropertyMappingEvaluator() evaluator = PropertyMappingEvaluator()
evaluator.set_context(user, request, **kwargs) evaluator.set_context(user, request, self, **kwargs)
try: try:
return evaluator.evaluate(self.expression) return evaluator.evaluate(self.expression)
except (ValueError, SyntaxError) as exc: except (ValueError, SyntaxError) as exc:
@ -490,8 +499,12 @@ class AuthenticatedSession(ExpiringModel):
last_used = models.DateTimeField(auto_now=True) last_used = models.DateTimeField(auto_now=True)
@staticmethod @staticmethod
def from_request(request: HttpRequest, user: User) -> "AuthenticatedSession": def from_request(
request: HttpRequest, user: User
) -> Optional["AuthenticatedSession"]:
"""Create a new session from a http request""" """Create a new session from a http request"""
if not hasattr(request, "session") or not request.session.session_key:
return None
return AuthenticatedSession( return AuthenticatedSession(
session_key=request.session.session_key, session_key=request.session.session_key,
user=user, user=user,

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)
@ -48,7 +49,9 @@ def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
"""Create an AuthenticatedSession from request""" """Create an AuthenticatedSession from request"""
from authentik.core.models import AuthenticatedSession from authentik.core.models import AuthenticatedSession
AuthenticatedSession.from_request(request, user).save() session = AuthenticatedSession.from_request(request, user)
if session:
session.save()
@receiver(user_logged_out) @receiver(user_logged_out)
@ -60,3 +63,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
@ -182,6 +183,8 @@ class SourceFlowManager:
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_stages_to_append(self, flow: Flow) -> list[Stage]: def get_stages_to_append(self, flow: Flow) -> list[Stage]:
"""Hook to override stages which are appended to the flow""" """Hook to override stages which are appended to the flow"""
if not self.source.enrollment_flow:
return []
if flow.slug == self.source.enrollment_flow.slug: if flow.slug == self.source.enrollment_flow.slug:
return [ return [
in_memory_stage(PostUserEnrollmentStage), in_memory_stage(PostUserEnrollmentStage),
@ -198,7 +201,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

@ -11,6 +11,11 @@
{% block head %} {% block head %}
<script src="{% static 'dist/FlowInterface.js' %}?v={{ ak_version }}" type="module"></script> <script src="{% static 'dist/FlowInterface.js' %}?v={{ ak_version }}" type="module"></script>
<style>
.pf-c-background-image::before {
background-image: url("{{ flow.background_url }}");
}
</style>
{% endblock %} {% endblock %}
{% block body %} {% block body %}

View File

@ -7,6 +7,14 @@
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}?v={{ ak_version }}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}?v={{ ak_version }}">
{% endblock %} {% endblock %}
{% block head %}
<style>
.pf-c-background-image::before {
background-image: url("/static/dist/assets/images/flow_background.jpg");
}
</style>
{% endblock %}
{% block body %} {% block body %}
<div class="pf-c-background-image"> <div class="pf-c-background-image">
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0"> <svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">

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

@ -55,11 +55,16 @@ class CertificateKeyPair(CreatedUpdatedModel):
def private_key(self) -> Optional[RSAPrivateKey]: def private_key(self) -> Optional[RSAPrivateKey]:
"""Get python cryptography PrivateKey instance""" """Get python cryptography PrivateKey instance"""
if not self._private_key and self._private_key != "": if not self._private_key and self._private_key != "":
try:
self._private_key = load_pem_private_key( self._private_key = load_pem_private_key(
str.encode("\n".join([x.strip() for x in self.key_data.split("\n")])), str.encode(
"\n".join([x.strip() for x in self.key_data.split("\n")])
),
password=None, password=None,
backend=default_backend(), backend=default_backend(),
) )
except ValueError:
return None
return self._private_key return self._private_key
@property @property

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

@ -14,6 +14,7 @@ from authentik.events.models import cleanse_dict
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.markers import ReevaluateMarker, StageMarker from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import Flow, FlowStageBinding, Stage from authentik.flows.models import Flow, FlowStageBinding, Stage
from authentik.lib.config import CONFIG
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.root.monitoring import UpdatingGauge from authentik.root.monitoring import UpdatingGauge
@ -33,6 +34,7 @@ HIST_FLOWS_PLAN_TIME = Histogram(
"Duration to build a plan for a flow", "Duration to build a plan for a flow",
["flow_slug"], ["flow_slug"],
) )
CACHE_TIMEOUT = int(CONFIG.y("redis.cache_timeout_flows"))
def cache_key(flow: Flow, user: Optional[User] = None) -> str: def cache_key(flow: Flow, user: Optional[User] = None) -> str:
@ -157,7 +159,7 @@ class FlowPlanner:
"f(plan): building plan", "f(plan): building plan",
) )
plan = self._build_plan(user, request, default_context) plan = self._build_plan(user, request, default_context)
cache.set(cache_key(self.flow, user), plan) cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
GAUGE_FLOWS_CACHED.update() GAUGE_FLOWS_CACHED.update()
if not plan.stages and not self.allow_empty_flows: if not plan.stages and not self.allow_empty_flows:
raise EmptyFlowException() raise EmptyFlowException()

View File

@ -18,27 +18,11 @@ 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.views import FlowExecutorView from authentik.flows.views import FlowExecutorView
from authentik.lib.sentry import SentryIgnoredException
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier" PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
LOGGER = get_logger() LOGGER = get_logger()
class InvalidChallengeError(SentryIgnoredException):
"""Error raised when a challenge from a stage is not valid"""
def __init__(self, errors, stage_view: View, challenge: Challenge) -> None:
super().__init__()
self.errors = errors
self.stage_view = stage_view
self.challenge = challenge
def __str__(self) -> str:
return (
f"Invalid challenge from {self.stage_view}: {self.errors}\n{self.challenge}"
)
class StageView(View): class StageView(View):
"""Abstract Stage, inherits TemplateView but can be combined with FormView""" """Abstract Stage, inherits TemplateView but can be combined with FormView"""
@ -50,14 +34,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 +96,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

@ -44,6 +44,7 @@ from authentik.flows.planner import (
FlowPlan, FlowPlan,
FlowPlanner, FlowPlanner,
) )
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.reflection import all_subclasses, class_to_path from authentik.lib.utils.reflection import all_subclasses, class_to_path
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
@ -93,6 +94,10 @@ def challenge_response_types():
return Inner() return Inner()
class InvalidStageError(SentryIgnoredException):
"""Error raised when a challenge from a stage is not valid"""
@method_decorator(xframe_options_sameorigin, name="dispatch") @method_decorator(xframe_options_sameorigin, name="dispatch")
class FlowExecutorView(APIView): class FlowExecutorView(APIView):
"""Stage 1 Flow executor, passing requests to Stage Views""" """Stage 1 Flow executor, passing requests to Stage Views"""
@ -164,17 +169,24 @@ class FlowExecutorView(APIView):
current_stage=self.current_stage, current_stage=self.current_stage,
flow_slug=self.flow.slug, flow_slug=self.flow.slug,
) )
try:
stage_cls = self.current_stage.type stage_cls = self.current_stage.type
except NotImplementedError as exc:
self._logger.debug("Error getting stage type", exc=exc)
return self.stage_invalid()
self.current_stage_view = stage_cls(self) self.current_stage_view = stage_cls(self)
self.current_stage_view.args = self.args self.current_stage_view.args = self.args
self.current_stage_view.kwargs = self.kwargs self.current_stage_view.kwargs = self.kwargs
self.current_stage_view.request = request self.current_stage_view.request = request
try:
return super().dispatch(request) return super().dispatch(request)
except InvalidStageError as exc:
return self.stage_invalid(str(exc))
@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 +226,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",
), ),
@ -353,8 +365,11 @@ class FlowErrorResponse(TemplateResponse):
context = {} context = {}
context["error"] = self.error context["error"] = self.error
if self._request.user and self._request.user.is_authenticated: if self._request.user and self._request.user.is_authenticated:
if self._request.user.is_superuser or self._request.user.attributes.get( if (
self._request.user.is_superuser
or self._request.user.group_attributes().get(
USER_ATTRIBUTE_DEBUG, False USER_ATTRIBUTE_DEBUG, False
)
): ):
context["tb"] = "".join(format_tb(self.error.__traceback__)) context["tb"] = "".join(format_tb(self.error.__traceback__))
return context return context

View File

@ -62,7 +62,7 @@ class ConfigLoader:
output.update(kwargs) output.update(kwargs)
print(dumps(output)) print(dumps(output))
def update(self, root, updatee): def update(self, root: dict[str, Any], updatee: dict[str, Any]) -> dict[str, Any]:
"""Recursively update dictionary""" """Recursively update dictionary"""
for key, value in updatee.items(): for key, value in updatee.items():
if isinstance(value, Mapping): if isinstance(value, Mapping):
@ -73,7 +73,7 @@ class ConfigLoader:
root[key] = value root[key] = value
return root return root
def parse_uri(self, value): def parse_uri(self, value: str) -> str:
"""Parse string values which start with a URI""" """Parse string values which start with a URI"""
url = urlparse(value) url = urlparse(value)
if url.scheme == "env": if url.scheme == "env":
@ -99,7 +99,10 @@ class ConfigLoader:
raise ImproperlyConfigured from exc raise ImproperlyConfigured from exc
except PermissionError as exc: except PermissionError as exc:
self._log( self._log(
"warning", "Permission denied while reading file", path=path, error=exc "warning",
"Permission denied while reading file",
path=path,
error=str(exc),
) )
def update_from_dict(self, update: dict): def update_from_dict(self, update: dict):

View File

@ -9,6 +9,7 @@ postgresql:
web: web:
listen: 0.0.0.0:9000 listen: 0.0.0.0:9000
listen_tls: 0.0.0.0:9443 listen_tls: 0.0.0.0:9443
load_local_files: false
redis: redis:
host: localhost host: localhost
@ -16,6 +17,10 @@ redis:
cache_db: 0 cache_db: 0
message_queue_db: 1 message_queue_db: 1
ws_db: 2 ws_db: 2
cache_timeout: 300
cache_timeout_flows: 300
cache_timeout_policies: 300
cache_timeout_reputation: 300
debug: false debug: false
@ -45,11 +50,11 @@ outposts:
# %(build_hash)s: Build hash if you're running a beta version # %(build_hash)s: Build hash if you're running a beta version
docker_image_base: "ghcr.io/goauthentik/%(type)s:%(version)s" docker_image_base: "ghcr.io/goauthentik/%(type)s:%(version)s"
authentik: avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar
avatars: gravatar # gravatar or none geoip: "./GeoLite2-City.mmdb"
geoip: "./GeoLite2-City.mmdb"
# Optionally add links to the footer on the login page # Can't currently be configured via environment variables, only yaml
footer_links: footer_links:
- name: Documentation - name: Documentation
href: https://goauthentik.io/docs/ href: https://goauthentik.io/docs/
- name: authentik Website - name: authentik Website

View File

@ -0,0 +1,61 @@
"""Test config loader"""
from os import chmod, environ, unlink, write
from tempfile import mkstemp
from django.conf import ImproperlyConfigured
from django.test import TestCase
from authentik.lib.config import ENV_PREFIX, ConfigLoader
class TestConfig(TestCase):
"""Test config loader"""
def test_env(self):
"""Test simple instance"""
config = ConfigLoader()
environ[ENV_PREFIX + "_test__test"] = "bar"
config.update_from_env()
self.assertEqual(config.y("test.test"), "bar")
def test_patch(self):
"""Test patch decorator"""
config = ConfigLoader()
config.y_set("foo.bar", "bar")
self.assertEqual(config.y("foo.bar"), "bar")
with config.patch("foo.bar", "baz"):
self.assertEqual(config.y("foo.bar"), "baz")
self.assertEqual(config.y("foo.bar"), "bar")
def test_uri_env(self):
"""Test URI parsing (environment)"""
config = ConfigLoader()
environ["foo"] = "bar"
self.assertEqual(config.parse_uri("env://foo"), "bar")
self.assertEqual(config.parse_uri("env://fo?bar"), "bar")
def test_uri_file(self):
"""Test URI parsing (file load)"""
config = ConfigLoader()
file, file_name = mkstemp()
write(file, "foo".encode())
_, file2_name = mkstemp()
chmod(file2_name, 0o000) # Remove all permissions so we can't read the file
self.assertEqual(config.parse_uri(f"file://{file_name}"), "foo")
self.assertEqual(config.parse_uri(f"file://{file2_name}?def"), "def")
unlink(file_name)
unlink(file2_name)
def test_file_update(self):
"""Test update_from_file"""
config = ConfigLoader()
file, file_name = mkstemp()
write(file, "{".encode())
file2, file2_name = mkstemp()
write(file2, "{".encode())
chmod(file2_name, 0o000) # Remove all permissions so we can't read the file
with self.assertRaises(ImproperlyConfigured):
config.update_from_file(file_name)
config.update_from_file(file2_name)
unlink(file_name)
unlink(file2_name)

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

@ -33,7 +33,7 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
return None return None
if OUTPOST_REMOTE_IP_HEADER not in request.META: if OUTPOST_REMOTE_IP_HEADER not in request.META:
return None return None
if request.user.attributes.get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False): if request.user.group_attributes().get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
return None return None
return request.META[OUTPOST_REMOTE_IP_HEADER] return request.META[OUTPOST_REMOTE_IP_HEADER]

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

@ -67,11 +67,6 @@ class OutpostConsumer(AuthJsonConsumer):
self.accept() self.accept()
self.outpost = outpost.first() self.outpost = outpost.first()
self.last_uid = self.channel_name self.last_uid = self.channel_name
LOGGER.debug(
"added outpost instace to cache",
outpost=self.outpost,
channel_name=self.channel_name,
)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def disconnect(self, close_code): def disconnect(self, close_code):
@ -108,6 +103,11 @@ class OutpostConsumer(AuthJsonConsumer):
outpost=self.outpost.name, outpost=self.outpost.name,
uid=self.last_uid, uid=self.last_uid,
).inc() ).inc()
LOGGER.debug(
"added outpost instace to cache",
outpost=self.outpost,
instance_uuid=self.last_uid,
)
self.first_msg = True self.first_msg = True
if msg.instruction == WebsocketMessageInstruction.HELLO: if msg.instruction == WebsocketMessageInstruction.HELLO:

View File

@ -63,10 +63,10 @@ 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.inner_port or port.port}/{port.protocol.lower()}": port.port
for port in self.deployment_ports for port in self.deployment_ports
}, },
"environment": self._get_env(), "environment": self._get_env(),

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

@ -37,7 +37,9 @@ class AccessDeniedResponse(TemplateResponse):
if self._request.user and self._request.user.is_authenticated: if self._request.user and self._request.user.is_authenticated:
if ( if (
self._request.user.is_superuser self._request.user.is_superuser
or self._request.user.attributes.get(USER_ATTRIBUTE_DEBUG, False) or self._request.user.group_attributes().get(
USER_ATTRIBUTE_DEBUG, False
)
): ):
context["policy_result"] = self.policy_result context["policy_result"] = self.policy_result
return context return context

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,14 +10,16 @@ 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.config import CONFIG
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")
CACHE_TIMEOUT = int(CONFIG.y("redis.cache_timeout_policies"))
PROCESS_CLASS = FORK_CTX.Process PROCESS_CLASS = FORK_CTX.Process
HIST_POLICIES_EXECUTION_TIME = Histogram( HIST_POLICIES_EXECUTION_TIME = Histogram(
"authentik_policies_execution_time", "authentik_policies_execution_time",
@ -106,11 +107,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)
@ -119,7 +116,7 @@ class PolicyProcess(PROCESS_CLASS):
policy_result.source_binding = self.binding policy_result.source_binding = self.binding
if not self.request.debug: if not self.request.debug:
key = cache_key(self.binding, self.request) key = cache_key(self.binding, self.request)
cache.set(key, policy_result) cache.set(key, policy_result, CACHE_TIMEOUT)
LOGGER.debug( LOGGER.debug(
"P_ENG(proc): finished and cached ", "P_ENG(proc): finished and cached ",
policy=self.binding.policy, policy=self.binding.policy,

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

@ -5,6 +5,7 @@ from django.dispatch import receiver
from django.http import HttpRequest from django.http import HttpRequest
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.lib.config import CONFIG
from authentik.lib.utils.http import get_client_ip from authentik.lib.utils.http import get_client_ip
from authentik.policies.reputation.models import ( from authentik.policies.reputation.models import (
CACHE_KEY_IP_PREFIX, CACHE_KEY_IP_PREFIX,
@ -13,6 +14,7 @@ from authentik.policies.reputation.models import (
from authentik.stages.identification.signals import identification_failed from authentik.stages.identification.signals import identification_failed
LOGGER = get_logger() LOGGER = get_logger()
CACHE_TIMEOUT = int(CONFIG.y("redis.cache_timeout_reputation"))
def update_score(request: HttpRequest, username: str, amount: int): def update_score(request: HttpRequest, username: str, amount: int):
@ -20,10 +22,10 @@ def update_score(request: HttpRequest, username: str, amount: int):
remote_ip = get_client_ip(request) remote_ip = get_client_ip(request)
# We only update the cache here, as its faster than writing to the DB # We only update the cache here, as its faster than writing to the DB
cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0) cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0, CACHE_TIMEOUT)
cache.incr(CACHE_KEY_IP_PREFIX + remote_ip, amount) cache.incr(CACHE_KEY_IP_PREFIX + remote_ip, amount)
cache.get_or_set(CACHE_KEY_USER_PREFIX + username, 0) cache.get_or_set(CACHE_KEY_USER_PREFIX + username, 0, CACHE_TIMEOUT)
cache.incr(CACHE_KEY_USER_PREFIX + username, amount) cache.incr(CACHE_KEY_USER_PREFIX + username, amount)
LOGGER.debug("Updated score", amount=amount, for_user=username, for_ip=remote_ip) LOGGER.debug("Updated score", amount=amount, for_user=username, for_ip=remote_ip)

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

@ -105,6 +105,7 @@ class PolicyAccessView(AccessMixin, View):
policy_engine = PolicyEngine( policy_engine = PolicyEngine(
self.application, user or self.request.user, self.request self.application, user or self.request.user, self.request
) )
policy_engine.use_cache = False
policy_engine.build() policy_engine.build()
result = policy_engine.result result = policy_engine.result
LOGGER.debug( LOGGER.debug(

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

@ -9,7 +9,7 @@ return {}
""" """
SCOPE_EMAIL_EXPRESSION = """ SCOPE_EMAIL_EXPRESSION = """
return { return {
"email": user.email, "email": request.user.email,
"email_verified": True "email_verified": True
} }
""" """
@ -17,14 +17,14 @@ SCOPE_PROFILE_EXPRESSION = """
return { return {
# Because authentik only saves the user's full name, and has no concept of first and last names, # Because authentik only saves the user's full name, and has no concept of first and last names,
# the full name is used as given name. # the full name is used as given name.
# You can override this behaviour in custom mappings, i.e. `user.name.split(" ")` # You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")`
"name": user.name, "name": request.user.name,
"given_name": user.name, "given_name": request.user.name,
"family_name": "", "family_name": "",
"preferred_username": user.username, "preferred_username": request.user.username,
"nickname": user.username, "nickname": request.user.username,
# groups is not part of the official userinfo schema, but is a quasi-standard # groups is not part of the official userinfo schema, but is a quasi-standard
"groups": [group.name for group in user.ak_groups.all()], "groups": [group.name for group in request.user.ak_groups.all()],
} }
""" """

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

@ -8,7 +8,7 @@ SCOPE_AK_PROXY_EXPRESSION = """
# which are used for example for the HTTP-Basic Authentication mapping. # which are used for example for the HTTP-Basic Authentication mapping.
return { return {
"ak_proxy": { "ak_proxy": {
"user_attributes": user.group_attributes() "user_attributes": request.user.group_attributes()
} }
}""" }"""

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

@ -3,7 +3,7 @@ from authentik.managed.manager import EnsureExists, ObjectManager
from authentik.providers.saml.models import SAMLPropertyMapping from authentik.providers.saml.models import SAMLPropertyMapping
GROUP_EXPRESSION = """ GROUP_EXPRESSION = """
for group in user.ak_groups.all(): for group in request.user.ak_groups.all():
yield group.name yield group.name
""" """
@ -18,7 +18,7 @@ class SAMLProviderManager(ObjectManager):
"goauthentik.io/providers/saml/upn", "goauthentik.io/providers/saml/upn",
name="authentik default SAML Mapping: UPN", name="authentik default SAML Mapping: UPN",
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn",
expression="return user.attributes.get('upn', user.email)", expression="return request.user.attributes.get('upn', request.user.email)",
friendly_name="", friendly_name="",
), ),
EnsureExists( EnsureExists(
@ -26,7 +26,7 @@ class SAMLProviderManager(ObjectManager):
"goauthentik.io/providers/saml/name", "goauthentik.io/providers/saml/name",
name="authentik default SAML Mapping: Name", name="authentik default SAML Mapping: Name",
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
expression="return user.name", expression="return request.user.name",
friendly_name="", friendly_name="",
), ),
EnsureExists( EnsureExists(
@ -34,7 +34,7 @@ class SAMLProviderManager(ObjectManager):
"goauthentik.io/providers/saml/email", "goauthentik.io/providers/saml/email",
name="authentik default SAML Mapping: Email", name="authentik default SAML Mapping: Email",
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
expression="return user.email", expression="return request.user.email",
friendly_name="", friendly_name="",
), ),
EnsureExists( EnsureExists(
@ -42,7 +42,7 @@ class SAMLProviderManager(ObjectManager):
"goauthentik.io/providers/saml/username", "goauthentik.io/providers/saml/username",
name="authentik default SAML Mapping: Username", name="authentik default SAML Mapping: Username",
saml_name="http://schemas.goauthentik.io/2021/02/saml/username", saml_name="http://schemas.goauthentik.io/2021/02/saml/username",
expression="return user.username", expression="return request.user.username",
friendly_name="", friendly_name="",
), ),
EnsureExists( EnsureExists(
@ -50,7 +50,7 @@ class SAMLProviderManager(ObjectManager):
"goauthentik.io/providers/saml/uid", "goauthentik.io/providers/saml/uid",
name="authentik default SAML Mapping: User ID", name="authentik default SAML Mapping: User ID",
saml_name="http://schemas.goauthentik.io/2021/02/saml/uid", saml_name="http://schemas.goauthentik.io/2021/02/saml/uid",
expression="return user.pk", expression="return request.user.pk",
friendly_name="", friendly_name="",
), ),
EnsureExists( EnsureExists(
@ -68,7 +68,7 @@ class SAMLProviderManager(ObjectManager):
saml_name=( saml_name=(
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname" "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
), ),
expression="return user.username", expression="return request.user.username",
friendly_name="", friendly_name="",
), ),
] ]

View File

@ -24,6 +24,7 @@ from authentik.sources.saml.processors.constants import (
SAML_NAME_ID_FORMAT_EMAIL, SAML_NAME_ID_FORMAT_EMAIL,
SAML_NAME_ID_FORMAT_PERSISTENT, SAML_NAME_ID_FORMAT_PERSISTENT,
SAML_NAME_ID_FORMAT_TRANSIENT, SAML_NAME_ID_FORMAT_TRANSIENT,
SAML_NAME_ID_FORMAT_UNSPECIFIED,
SAML_NAME_ID_FORMAT_WINDOWS, SAML_NAME_ID_FORMAT_WINDOWS,
SAML_NAME_ID_FORMAT_X509, SAML_NAME_ID_FORMAT_X509,
SIGN_ALGORITHM_TRANSFORM_MAP, SIGN_ALGORITHM_TRANSFORM_MAP,
@ -165,7 +166,10 @@ class AssertionProcessor:
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_EMAIL: if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_EMAIL:
name_id.text = self.http_request.user.email name_id.text = self.http_request.user.email
return name_id return name_id
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_PERSISTENT: if name_id.attrib["Format"] in [
SAML_NAME_ID_FORMAT_PERSISTENT,
SAML_NAME_ID_FORMAT_UNSPECIFIED,
]:
name_id.text = persistent name_id.text = persistent
return name_id return name_id
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_X509: if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_X509:
@ -180,7 +184,7 @@ class AssertionProcessor:
return name_id return name_id
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT: if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT:
# Use the hash of the user's session, which changes every session # Use the hash of the user's session, which changes every session
session_key: str = self.http_request.user.session.session_key session_key: str = self.http_request.session.session_key
name_id.text = sha256(session_key.encode()).hexdigest() name_id.text = sha256(session_key.encode()).hexdigest()
return name_id return name_id
raise UnsupportedNameIDFormat( raise UnsupportedNameIDFormat(

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

@ -20,10 +20,11 @@ from authentik.sources.saml.processors.constants import (
RSA_SHA256, RSA_SHA256,
RSA_SHA384, RSA_SHA384,
RSA_SHA512, RSA_SHA512,
SAML_NAME_ID_FORMAT_EMAIL, SAML_NAME_ID_FORMAT_UNSPECIFIED,
) )
LOGGER = get_logger() LOGGER = get_logger()
ERROR_CANNOT_DECODE_REQUEST = "Cannot decode SAML request."
ERROR_SIGNATURE_REQUIRED_BUT_ABSENT = ( ERROR_SIGNATURE_REQUIRED_BUT_ABSENT = (
"Verification Certificate configured, but request is not signed." "Verification Certificate configured, but request is not signed."
) )
@ -42,7 +43,7 @@ class AuthNRequest:
relay_state: Optional[str] = None relay_state: Optional[str] = None
name_id_policy: str = SAML_NAME_ID_FORMAT_EMAIL name_id_policy: str = SAML_NAME_ID_FORMAT_UNSPECIFIED
class AuthNRequestParser: class AuthNRequestParser:
@ -69,16 +70,21 @@ class AuthNRequestParser:
auth_n_request = AuthNRequest(id=root.attrib["ID"], relay_state=relay_state) auth_n_request = AuthNRequest(id=root.attrib["ID"], relay_state=relay_state)
# Check if AuthnRequest has a NameID Policy object # Check if AuthnRequest has a NameID Policy object
name_id_policies = root.findall(f"{{{NS_SAML_PROTOCOL}}}:NameIDPolicy") name_id_policies = root.findall(f"{{{NS_SAML_PROTOCOL}}}NameIDPolicy")
if len(name_id_policies) > 0: if len(name_id_policies) > 0:
name_id_policy = name_id_policies[0] name_id_policy = name_id_policies[0]
auth_n_request.name_id_policy = name_id_policy.attrib["Format"] auth_n_request.name_id_policy = name_id_policy.attrib.get(
"Format", SAML_NAME_ID_FORMAT_UNSPECIFIED
)
return auth_n_request return auth_n_request
def parse(self, saml_request: str, relay_state: Optional[str]) -> AuthNRequest: def parse(self, saml_request: str, relay_state: Optional[str]) -> AuthNRequest:
"""Validate and parse raw request with enveloped signautre.""" """Validate and parse raw request with enveloped signautre."""
try:
decoded_xml = b64decode(saml_request.encode()).decode() decoded_xml = b64decode(saml_request.encode()).decode()
except UnicodeDecodeError:
raise CannotHandleAssertion(ERROR_CANNOT_DECODE_REQUEST)
verifier = self.provider.verification_kp verifier = self.provider.verification_kp
@ -108,7 +114,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)
@ -121,7 +127,10 @@ class AuthNRequestParser:
sig_alg: Optional[str] = None, sig_alg: Optional[str] = None,
) -> AuthNRequest: ) -> AuthNRequest:
"""Validate and parse raw request with detached signature""" """Validate and parse raw request with detached signature"""
try:
decoded_xml = decode_base64_and_inflate(saml_request) decoded_xml = decode_base64_and_inflate(saml_request)
except UnicodeDecodeError:
raise CannotHandleAssertion(ERROR_CANNOT_DECODE_REQUEST)
verifier = self.provider.verification_kp verifier = self.provider.verification_kp
@ -160,7 +169,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,18 +2,19 @@
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
from authentik.sources.saml.exceptions import MismatchedRequestID from authentik.sources.saml.exceptions import MismatchedRequestID
from authentik.sources.saml.models import SAMLSource from authentik.sources.saml.models import SAMLSource
from authentik.sources.saml.processors.constants import SAML_NAME_ID_FORMAT_EMAIL from authentik.sources.saml.processors.constants import SAML_NAME_ID_FORMAT_UNSPECIFIED
from authentik.sources.saml.processors.request import ( from authentik.sources.saml.processors.request import (
SESSION_REQUEST_ID, SESSION_REQUEST_ID,
RequestProcessor, RequestProcessor,
@ -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"""
@ -210,5 +206,5 @@ class TestAuthNRequest(TestCase):
REDIRECT_REQUEST, REDIRECT_RELAY_STATE, REDIRECT_SIGNATURE, REDIRECT_SIG_ALG REDIRECT_REQUEST, REDIRECT_RELAY_STATE, REDIRECT_SIGNATURE, REDIRECT_SIG_ALG
) )
self.assertEqual(parsed_request.id, "_dcf55fcd27a887e60a7ef9ee6fd3adab") self.assertEqual(parsed_request.id, "_dcf55fcd27a887e60a7ef9ee6fd3adab")
self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_EMAIL) self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_UNSPECIFIED)
self.assertEqual(parsed_request.relay_state, REDIRECT_RELAY_STATE) self.assertEqual(parsed_request.relay_state, REDIRECT_RELAY_STATE)

View File

@ -17,6 +17,7 @@ from authentik.providers.saml.models import SAMLBindings, 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 AuthNRequest from authentik.providers.saml.processors.request_parser import AuthNRequest
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode, nice64 from authentik.providers.saml.utils.encoding import deflate_and_base64_encode, nice64
from authentik.sources.saml.exceptions import SAMLException
LOGGER = get_logger() LOGGER = get_logger()
URL_VALIDATOR = URLValidator(schemes=("http", "https")) URL_VALIDATOR = URLValidator(schemes=("http", "https"))
@ -56,22 +57,30 @@ class SAMLFlowFinalView(ChallengeStageView):
provider: SAMLProvider = get_object_or_404( provider: SAMLProvider = get_object_or_404(
SAMLProvider, pk=application.provider_id SAMLProvider, pk=application.provider_id
) )
# Log Application Authorization
Event.new(
EventAction.AUTHORIZE_APPLICATION,
authorized_application=application,
flow=self.executor.plan.flow_pk,
).from_http(self.request)
if SESSION_KEY_AUTH_N_REQUEST not in self.request.session: if SESSION_KEY_AUTH_N_REQUEST not in self.request.session:
return self.executor.stage_invalid() return self.executor.stage_invalid()
auth_n_request: AuthNRequest = self.request.session.pop( auth_n_request: AuthNRequest = self.request.session.pop(
SESSION_KEY_AUTH_N_REQUEST SESSION_KEY_AUTH_N_REQUEST
) )
try:
response = AssertionProcessor( response = AssertionProcessor(
provider, request, auth_n_request provider, request, auth_n_request
).build_response() ).build_response()
except SAMLException as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message=f"Failed to process SAML assertion: {str(exc)}",
provider=provider,
).from_http(self.request)
return self.executor.stage_invalid()
# Log Application Authorization
Event.new(
EventAction.AUTHORIZE_APPLICATION,
authorized_application=application,
flow=self.executor.plan.flow_pk,
).from_http(self.request)
if provider.sp_binding == SAMLBindings.POST: if provider.sp_binding == SAMLBindings.POST:
form_attrs = { form_attrs = {

View File

@ -44,7 +44,7 @@ class Command(BaseCommand):
user=user, user=user,
intent=TokenIntents.INTENT_RECOVERY, intent=TokenIntents.INTENT_RECOVERY,
description=f"Recovery Token generated by {getuser()} on {_now}", description=f"Recovery Token generated by {getuser()} on {_now}",
identifier=f"ak-recovery-{user}", identifier=f"ak-recovery-{user}-{_now}",
) )
self.stdout.write( self.stdout.write(
( (

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

@ -15,6 +15,7 @@ import logging
import os import os
import sys import sys
from json import dumps from json import dumps
from tempfile import gettempdir
from time import time from time import time
import structlog import structlog
@ -193,6 +194,7 @@ CACHES = {
f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:6379" f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:6379"
f"/{CONFIG.y('redis.cache_db')}" f"/{CONFIG.y('redis.cache_db')}"
), ),
"TIMEOUT": int(CONFIG.y("redis.cache_timeout", 300)),
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
} }
} }
@ -341,7 +343,7 @@ DBBACKUP_FILENAME_TEMPLATE = "authentik-backup-{datetime}.sql"
DBBACKUP_CONNECTOR_MAPPING = { DBBACKUP_CONNECTOR_MAPPING = {
"django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector", "django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
} }
DBBACKUP_TMP_DIR = gettempdir() if DEBUG else "/tmp" # nosec
if CONFIG.y("postgresql.s3_backup"): if CONFIG.y("postgresql.s3_backup"):
DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
DBBACKUP_STORAGE_OPTIONS = { DBBACKUP_STORAGE_OPTIONS = {
@ -375,7 +377,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"
) )

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