Compare commits

...

196 Commits

Author SHA1 Message Date
680b182d95 release: 2021.6.3 2021-06-29 16:19:07 +02:00
b2a832175e build(deps): bump celery from 5.1.1 to 5.1.2 (#1092) 2021-06-29 08:55:13 +02:00
b3ce8331f5 build(deps): bump @typescript-eslint/parser in /web (#1087) 2021-06-29 08:55:00 +02:00
ef0f618234 build(deps): bump @sentry/tracing from 6.7.2 to 6.8.0 in /web (#1089) 2021-06-29 08:54:49 +02:00
b8a7186a55 build(deps): bump @typescript-eslint/eslint-plugin in /web (#1088) 2021-06-29 08:53:42 +02:00
b39530f873 build(deps): bump @sentry/browser from 6.7.2 to 6.8.0 in /web (#1090) 2021-06-29 08:53:31 +02:00
7937c84f2b build(deps): bump boto3 from 1.17.101 to 1.17.102 (#1091) 2021-06-29 08:53:10 +02:00
621843c60c flows: fix migration dependency issue
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-28 23:55:07 +02:00
c19da839b1 stages/user_write: add create_users_as_inactive flag
close #1086

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-28 23:24:54 +02:00
fea1f3be6f stages/prompt: ensure hidden and static fields keep the value they had set
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-28 22:29:36 +02:00
6f5ec7838f events: fix linting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-28 20:57:28 +02:00
94300492e7 website/docs: update release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-28 20:27:22 +02:00
5d3931c128 events: ignore notification non-existent in transport
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-28 20:15:00 +02:00
262a8b5ae8 api: use partition instead of split for token
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-28 20:13:08 +02:00
fe069c5e55 website/docs: fix use of escaped_request_uri in standalone nginx
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-28 19:51:55 +02:00
c6e60c0ebc build(deps): bump rollup from 2.52.2 to 2.52.3 in /web (#1080) 2021-06-28 08:53:15 +02:00
90b457c5ee build(deps-dev): bump prettier from 2.3.1 to 2.3.2 in /website (#1081) 2021-06-28 08:53:07 +02:00
5e724e4299 build(deps): bump chart.js from 3.3.2 to 3.4.0 in /web (#1082) 2021-06-28 08:52:54 +02:00
b4c8dd6b91 build(deps): bump boto3 from 1.17.100 to 1.17.101 (#1083) 2021-06-28 08:52:31 +02:00
63d163cc65 build(deps): bump urllib3 from 1.26.5 to 1.26.6 (#1084) 2021-06-28 08:52:21 +02:00
2b1356bb91 flows: add invalid_response_action to configure how the FlowExecutor should handle invalid responses
closes #1079

Default value of `retry` behaves like previous version.

`restart` and `restart_with_context` restart the flow upon an invalid response. `restart_with_context` keeps the same context of the Flow, allowing users to bind policies that maybe aren't valid on the first execution, but are after a retry, like a reputation policy with a deny stage.

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-28 00:22:09 +02:00
ba9edd6c44 flows: handle possible errors with FlowPlans received from cache
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-27 22:03:48 +02:00
3b2b3262d7 flows: add FlowStageBinding to flow plan instead of just stage
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-27 18:47:04 +02:00
5431e7fe9d tenants: fix tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-27 15:12:47 +02:00
7d9c74ce04 tenants: include all default flows in current_tenant
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-26 23:47:49 +02:00
60c3cf890a events: add ability to create events via API
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-26 23:37:03 +02:00
4ec5df6b12 web/admin: fix linting error
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-26 22:30:33 +02:00
0403f6d373 web/admin: add flow export button on flow view page
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-26 22:03:19 +02:00
b7f4d15a94 web/admin: fix deletion of authenticator not reloading the state correctly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-26 21:22:10 +02:00
56450887ca web/admin: cleanup imports
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-26 21:14:23 +02:00
9bd613a31d stages/authenticator_duo: fix component not being set in API
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-26 20:49:58 +02:00
3fe0483dbf core: fix flow background not correctly loading on initial draw
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-26 20:29:45 +02:00
63a28ca1e9 web/admin: fix only recovery flows being selectable for unenrollment flow in tenant form
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-26 19:33:20 +02:00
2543b075be outposts/ldap: fixed IsActive and IsSuperuser returning swapped incorrect values (#1078)
IsActive and IsSuperuser attributes were interchanged.
2021-06-26 15:07:43 +02:00
b8bdf7a035 outposts: fix outpost being re-created when in host mode
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-25 15:15:18 +02:00
a3ff7cea23 providers/oauth2: fix usage of timedelta.seconds
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-25 11:55:00 +02:00
bb776c2710 outposts: check docker container ports match
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-25 11:54:35 +02:00
c9ad87d419 build(deps): bump boto3 from 1.17.99 to 1.17.100 (#1077)
Bumps [boto3](https://github.com/boto/boto3) from 1.17.99 to 1.17.100.
- [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.99...1.17.100)

---
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-25 10:59:40 +02:00
0d81eaffff web/admin: fix text color on pf-c-card
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-24 19:30:16 +02:00
6930c84425 events: only create SYSTEM_EXCEPTION event when error would've been sent to sentry
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-24 13:01:41 +02:00
eaaeaccf5d build(deps): bump boto3 from 1.17.98 to 1.17.99 (#1076)
Bumps [boto3](https://github.com/boto/boto3) from 1.17.98 to 1.17.99.
- [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.98...1.17.99)

---
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-24 09:58:23 +02:00
efbbd0adcf build(deps): bump @types/codemirror from 5.60.0 to 5.60.1 in /web (#1074)
Bumps [@types/codemirror](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/codemirror) from 5.60.0 to 5.60.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/codemirror)

---
updated-dependencies:
- dependency-name: "@types/codemirror"
  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-24 09:58:14 +02:00
c8d9771640 build(deps): bump @patternfly/patternfly from 4.108.2 to 4.115.2 in /web (#1075)
Bumps [@patternfly/patternfly](https://github.com/patternfly/patternfly) from 4.108.2 to 4.115.2.
- [Release notes](https://github.com/patternfly/patternfly/releases)
- [Changelog](https://github.com/patternfly/patternfly/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/patternfly/patternfly/compare/prerelease-v4.108.2...prerelease-v4.115.2)

---
updated-dependencies:
- dependency-name: "@patternfly/patternfly"
  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-24 09:58:06 +02:00
2b98637ca5 lib: fix regex_match result being inverted, add tests
closes #1073

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-23 20:06:43 +02:00
e3f7185564 website/docs: Added setting for SP name ID format (#1072) 2021-06-23 18:02:49 +02:00
d1198fc6c1 sources/ldap: improve error handling when checking for password complexity on non-ad setups
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1067
2021-06-23 00:24:05 +02:00
8cb5f8fbee Merge branch 'version-2021.6' 2021-06-22 23:58:54 +02:00
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
fad5b09aee website/docs: add release notes for 2021.6.2
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-22 23:18:05 +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
239 changed files with 3844 additions and 1464 deletions

View File

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

View File

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

1
.github/stale.yml vendored
View File

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

View File

@ -33,12 +33,21 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik:2021.6.1-rc3,
beryju/authentik:2021.6.3,
beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.6.1-rc3,
ghcr.io/goauthentik/server:2021.6.3,
ghcr.io/goauthentik/server:latest
platforms: linux/amd64,linux/arm64
context: .
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.6.3', '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:
runs-on: ubuntu-latest
steps:
@ -66,12 +75,21 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-proxy:2021.6.1-rc3,
beryju/authentik-proxy:2021.6.3,
beryju/authentik-proxy:latest,
ghcr.io/goauthentik/proxy:2021.6.1-rc3,
ghcr.io/goauthentik/proxy:2021.6.3,
ghcr.io/goauthentik/proxy:latest
file: outpost/proxy.Dockerfile
platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.6.3', '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:
runs-on: ubuntu-latest
steps:
@ -99,14 +117,22 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-ldap:2021.6.1-rc3,
beryju/authentik-ldap:2021.6.3,
beryju/authentik-ldap:latest,
ghcr.io/goauthentik/ldap:2021.6.1-rc3,
ghcr.io/goauthentik/ldap:2021.6.3,
ghcr.io/goauthentik/ldap:latest
file: outpost/ldap.Dockerfile
platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.6.3', '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:
if: ${{ github.event_name == 'release' }}
needs:
- build-server
- build-proxy
@ -130,13 +156,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- 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
uses: getsentry/action-release@v1
if: ${{ github.event_name == 'release' }}
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: beryjuorg
SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org
with:
version: authentik@2021.6.1-rc3
version: authentik@2021.6.3
environment: beryjuorg-prod
sourcemaps: './web/dist'

View File

@ -20,7 +20,7 @@ jobs:
docker-compose pull -q
docker build \
--no-cache \
-t beryju/authentik:latest \
-t ghcr.io/goauthentik/server:latest \
-f Dockerfile .
docker-compose up --no-start
docker-compose start postgresql redis

4
.gitignore vendored
View File

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

View File

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

259
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "4fa1ad681762c867a95410074f31ac5d00119e187e0f38982cd59fdf301cccf5"
"sha256": "f90d9fb4713eaf9c5ffe6a3858e64843670f79ab5007e7debf914c1f094c8d63"
},
"pipfile-spec": 6,
"requires": {
@ -56,6 +56,7 @@
"sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a",
"sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"
],
"markers": "python_version >= '3.6'",
"version": "==3.7.4.post0"
},
"aioredis": {
@ -70,20 +71,23 @@
"sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2",
"sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"
],
"markers": "python_version >= '3.6'",
"version": "==5.0.6"
},
"asgiref": {
"hashes": [
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
"sha256:05914d0fa65a21711e732adc6572edad6c8da5f1435c3f0c060689ced5e85195",
"sha256:d36fa91dd90e3aa3c81a6bd426ccc8fb20bd3d22b0cf14a12800289e9c3e2563"
],
"version": "==3.3.4"
"markers": "python_version >= '3.6'",
"version": "==3.4.0"
},
"async-timeout": {
"hashes": [
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
"markers": "python_full_version >= '3.5.3'",
"version": "==3.0.1"
},
"attrs": {
@ -91,6 +95,7 @@
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"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"
},
"autobahn": {
@ -98,6 +103,7 @@
"sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac",
"sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03"
],
"markers": "python_version >= '3.7'",
"version": "==21.3.1"
},
"automat": {
@ -116,23 +122,26 @@
},
"boto3": {
"hashes": [
"sha256:4cfab400cd9ca9b27b7dffb43f5675525ea5d36c81223d64d15542fdb16cdf7e",
"sha256:b0808a58c54c595b6cc6271cbc14a09bb89f0951ca9e8b105d1e94bef3ed24a0"
"sha256:6300e9ee9a404038113250bd218e2c4827f5e676efb14e77de2ad2dcb67679bc",
"sha256:be4714f0475c1f5183eea09ddbf568ced6fa41b0fc9976f2698b8442e1b17303"
],
"index": "pypi",
"version": "==1.17.91"
"version": "==1.17.102"
},
"botocore": {
"hashes": [
"sha256:462e75419e6537efb2709b7eb5b8c7ade007d30209416f0476bd7c51a2ddc78d"
"sha256:2f57f7ceed1598d96cc497aeb45317db5d3b21a5aafea4732d0e561d0fc2a8fa",
"sha256:bdf08a4f7f01ead00d386848f089c08270499711447569c18d0db60023619c06"
],
"version": "==1.20.91"
"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.102"
},
"cachetools": {
"hashes": [
"sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001",
"sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"
],
"markers": "python_version ~= '3.5'",
"version": "==4.2.2"
},
"cbor2": {
@ -151,15 +160,16 @@
"sha256:ce6219986385778b1ab7f9b542f160bb4d3558f52975e914a27b774e47016fb7",
"sha256:d562b2773e14ee1d65ea5b85351a83a64d4f3fd011bc2b4c70a6e813e78203ce"
],
"markers": "python_version >= '3.6'",
"version": "==5.4.0"
},
"celery": {
"hashes": [
"sha256:1329de1edeaf734ef859e630cb42df2c116d53e59d2f46433b13aed196e85620",
"sha256:65f061c04578cf189cd7352c192e1a79fdeb370b916bff792bcc769560e81184"
"sha256:8d9a3de9162965e97f8e8cc584c67aad83b3f7a267584fa47701ed11c3e0d4b0",
"sha256:9dab2170b4038f7bf10ef2861dbf486ddf1d20592290a1040f7b7a1259705d42"
],
"index": "pypi",
"version": "==5.1.0"
"version": "==5.1.2"
},
"certifi": {
"hashes": [
@ -243,6 +253,7 @@
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"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"
},
"click": {
@ -250,6 +261,7 @@
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"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"
},
"click-didyoumean": {
@ -309,8 +321,17 @@
"sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f",
"sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.2"
},
"deepmerge": {
"hashes": [
"sha256:87166dbe9ba1a3348a45c9d4ada6778f518d41afc0b85aa017ea3041facc3f9c",
"sha256:f6fd7f1293c535fb599e197e750dbe8674503c5d2a89759b3c72a3c46746d4fd"
],
"index": "pypi",
"version": "==0.3.0"
},
"defusedxml": {
"hashes": [
"sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
@ -413,11 +434,11 @@
},
"drf-spectacular": {
"hashes": [
"sha256:4d35e890b8139e1c056588c5529a2f2066615635482563f0840b96d3b879d7d2",
"sha256:f552476dfde647963c21615249672e7f4f9ece3788036b5ee5c6cc5ad50748ab"
"sha256:6ffbfde7d96a4a2febd19182cc405217e1e86a50280fc739402291c93d1a32b7",
"sha256:77593024bb899f69227abedcf87def7851a11c9978f781aa4b385a10f67a38b7"
],
"index": "pypi",
"version": "==0.17.0"
"version": "==0.17.2"
},
"duo-client": {
"hashes": [
@ -439,6 +460,7 @@
"hashes": [
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.18.2"
},
"geoip2": {
@ -451,10 +473,11 @@
},
"google-auth": {
"hashes": [
"sha256:9b235dbc876e49454cbedc52ae0abd540ef705ebccdf4fbe93553bb13f26b1a4",
"sha256:eb017521276a75492282c6ca4b718f26de112ed3bcbeaeeb02c1b82de425f909"
"sha256:b3a67fa9ba5b768861dacf374c2135eb09fa14a0e40c851c3b8ea7abe6fc8fef",
"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": {
"hashes": [
@ -469,6 +492,7 @@
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
],
"markers": "python_version >= '3.6'",
"version": "==0.12.0"
},
"hiredis": {
@ -515,6 +539,7 @@
"sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0",
"sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"
],
"markers": "python_version >= '3.6'",
"version": "==2.0.0"
},
"httptools": {
@ -563,6 +588,7 @@
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
],
"markers": "python_version >= '3.5'",
"version": "==0.5.1"
},
"jmespath": {
@ -570,6 +596,7 @@
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.0"
},
"jsonschema": {
@ -584,6 +611,7 @@
"sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d",
"sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a"
],
"markers": "python_version >= '3.6'",
"version": "==5.1.0"
},
"kubernetes": {
@ -597,6 +625,9 @@
"ldap3": {
"hashes": [
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91",
"sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59",
"sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c",
"sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056",
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57"
],
"index": "pypi",
@ -658,6 +689,7 @@
"hashes": [
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
],
"markers": "python_version >= '3.6'",
"version": "==2.0.3"
},
"msgpack": {
@ -733,6 +765,7 @@
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
],
"markers": "python_version >= '3.6'",
"version": "==5.1.0"
},
"oauthlib": {
@ -740,6 +773,7 @@
"sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc",
"sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"
],
"markers": "python_version >= '3.6'",
"version": "==3.1.1"
},
"packaging": {
@ -755,67 +789,85 @@
"sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86",
"sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.11.0"
},
"prompt-toolkit": {
"hashes": [
"sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04",
"sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc"
"sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f",
"sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"
],
"version": "==3.0.18"
"markers": "python_full_version >= '3.6.1'",
"version": "==3.0.19"
},
"psycopg2-binary": {
"hashes": [
"sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
"sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
"sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
"sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6",
"sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
"sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
"sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
"sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056",
"sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
"sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
"sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
"sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679",
"sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83",
"sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
"sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
"sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
"sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2",
"sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd",
"sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859",
"sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1",
"sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25",
"sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152",
"sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf",
"sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f",
"sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729",
"sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71",
"sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66",
"sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4",
"sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449",
"sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da",
"sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a",
"sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c",
"sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb",
"sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4",
"sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"
"sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975",
"sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd",
"sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616",
"sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2",
"sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90",
"sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a",
"sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e",
"sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d",
"sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed",
"sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a",
"sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140",
"sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32",
"sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31",
"sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a",
"sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917",
"sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf",
"sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7",
"sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0",
"sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72",
"sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698",
"sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773",
"sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68",
"sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76",
"sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4",
"sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f",
"sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34",
"sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce",
"sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a",
"sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e"
],
"index": "pypi",
"version": "==2.8.6"
"version": "==2.9.1"
},
"pyasn1": {
"hashes": [
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
],
"version": "==0.4.8"
},
"pyasn1-modules": {
"hashes": [
"sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8",
"sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",
"sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",
"sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",
"sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4",
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",
"sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",
"sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
"sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd",
"sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",
"sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",
"sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"
],
"version": "==0.2.8"
},
@ -824,6 +876,7 @@
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.20"
},
"pycryptodome": {
@ -867,6 +920,7 @@
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
],
"markers": "python_version >= '3.5'",
"version": "==2.0.2"
},
"pyjwt": {
@ -889,27 +943,50 @@
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7"
},
"pyrsistent": {
"hashes": [
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
"sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2",
"sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7",
"sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea",
"sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426",
"sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710",
"sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1",
"sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396",
"sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2",
"sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680",
"sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35",
"sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427",
"sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b",
"sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b",
"sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f",
"sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef",
"sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c",
"sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4",
"sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d",
"sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78",
"sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b",
"sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"
],
"version": "==0.17.3"
"markers": "python_version >= '3.6'",
"version": "==0.18.0"
},
"python-dateutil": {
"hashes": [
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.1"
},
"python-dotenv": {
"hashes": [
"sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544",
"sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"
"sha256:dd8fe852847f4fbfadabf6183ddd4c824a9651f02d51714fa075c95561959c7d",
"sha256:effaac3c1e58d89b3ccb4d04a40dc7ad6e0275fda25fd75ae9d323e2465e202d"
],
"version": "==0.17.1"
"version": "==0.18.0"
},
"pytz": {
"hashes": [
@ -958,6 +1035,7 @@
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
"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"
},
"requests": {
@ -965,12 +1043,14 @@
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"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"
},
"requests-oauthlib": {
"hashes": [
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
],
"index": "pypi",
"version": "==1.3.0"
@ -1011,6 +1091,7 @@
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"sqlparse": {
@ -1018,6 +1099,7 @@
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
],
"markers": "python_version >= '3.5'",
"version": "==0.4.1"
},
"structlog": {
@ -1073,6 +1155,7 @@
"sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8",
"sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb"
],
"markers": "python_version >= '3.6'",
"version": "==21.2.1"
},
"typing-extensions": {
@ -1096,6 +1179,7 @@
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.0.1"
},
"urllib3": {
@ -1103,11 +1187,11 @@
"secure"
],
"hashes": [
"sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c",
"sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"
"sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
"sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
],
"index": "pypi",
"version": "==1.26.5"
"version": "==1.26.6"
},
"uvicorn": {
"extras": [
@ -1140,6 +1224,7 @@
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
],
"markers": "python_version >= '3.6'",
"version": "==5.0.0"
},
"watchgod": {
@ -1169,6 +1254,7 @@
"sha256:b68e4959d704768fa20e35c9d508c8dc2bbc041fd8d267c0d7345cffe2824568",
"sha256:e5c333bfa9fa739538b652b6f8c8fc2559f1d364243c8a689d7c0e1d41c2e611"
],
"markers": "python_version >= '3.6'",
"version": "==1.1.0"
},
"websockets": {
@ -1266,6 +1352,7 @@
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
],
"markers": "python_version >= '3.6'",
"version": "==1.6.3"
},
"zope.interface": {
@ -1322,6 +1409,7 @@
"sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4",
"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"
}
},
@ -1338,6 +1426,7 @@
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
],
"markers": "python_version ~= '3.6'",
"version": "==2.5.6"
},
"attrs": {
@ -1345,6 +1434,7 @@
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"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"
},
"bandit": {
@ -1383,6 +1473,7 @@
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"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"
},
"click": {
@ -1390,6 +1481,7 @@
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"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"
},
"colorama": {
@ -1463,14 +1555,16 @@
"sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0",
"sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"
],
"markers": "python_version >= '3.4'",
"version": "==4.0.7"
},
"gitpython": {
"hashes": [
"sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135",
"sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"
"sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b",
"sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8"
],
"version": "==3.1.17"
"markers": "python_version >= '3.6'",
"version": "==3.1.18"
},
"idna": {
"hashes": [
@ -1488,10 +1582,11 @@
},
"isort": {
"hashes": [
"sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6",
"sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"
"sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56",
"sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"
],
"version": "==5.8.0"
"markers": "python_version < '4' and python_full_version >= '3.6.1'",
"version": "==5.9.1"
},
"lazy-object-proxy": {
"hashes": [
@ -1518,6 +1613,7 @@
"sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93",
"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"
},
"mccabe": {
@ -1554,6 +1650,7 @@
"sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd",
"sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"
],
"markers": "python_version >= '2.6'",
"version": "==5.6.0"
},
"pluggy": {
@ -1561,6 +1658,7 @@
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.13.1"
},
"py": {
@ -1568,6 +1666,7 @@
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.10.0"
},
"pylint": {
@ -1598,6 +1697,7 @@
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7"
},
"pytest": {
@ -1702,6 +1802,7 @@
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"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"
},
"requests-mock": {
@ -1725,6 +1826,7 @@
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"smmap": {
@ -1732,6 +1834,7 @@
"sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182",
"sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"
],
"markers": "python_version >= '3.5'",
"version": "==4.0.0"
},
"stevedore": {
@ -1739,6 +1842,7 @@
"sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee",
"sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"
],
"markers": "python_version >= '3.6'",
"version": "==3.3.0"
},
"toml": {
@ -1746,6 +1850,7 @@
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2"
},
"urllib3": {
@ -1753,11 +1858,11 @@
"secure"
],
"hashes": [
"sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c",
"sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"
"sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
"sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
],
"index": "pypi",
"version": "==1.26.5"
"version": "==1.26.6"
},
"wrapt": {
"hashes": [

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 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

View File

@ -1,3 +1,3 @@
"""authentik"""
__version__ = "2021.6.1-rc3"
__version__ = "2021.6.3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -10,3 +10,25 @@ class AuthentikAPIConfig(AppConfig):
label = "authentik_api"
mountpoint = "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 typing import Any, Optional, Union
from drf_spectacular.authentication import OpenApiAuthenticationExtension
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.request import Request
@ -20,7 +19,7 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
auth_credentials = raw_header.decode()
if auth_credentials == "" or " " not in auth_credentials:
return None
auth_type, auth_credentials = auth_credentials.split()
auth_type, _, auth_credentials = auth_credentials.partition(" ")
if auth_type.lower() not in ["basic", "bearer"]:
LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower())
raise AuthenticationFailed("Unsupported authentication type")
@ -56,18 +55,3 @@ class TokenAuthentication(BaseAuthentication):
return None
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,
)
from rest_framework.decorators import action
from rest_framework.fields import (
BooleanField,
CharField,
FileField,
IntegerField,
ReadOnlyField,
)
from rest_framework.fields import BooleanField, CharField, FileField, ReadOnlyField
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
@ -107,15 +101,19 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
return applications
@extend_schema(
request=inline_serializer(
"CheckAccessRequest", fields={"for_user": IntegerField(required=False)}
),
parameters=[
OpenApiParameter(
name="for_user",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.INT,
)
],
responses={
200: PolicyTestResultSerializer(),
404: OpenApiResponse(description="for_user user not found"),
},
)
@action(detail=True, methods=["POST"])
@action(detail=True, methods=["GET"])
# pylint: disable=unused-argument
def check_access(self, request: Request, slug: str) -> Response:
"""Check access to a single application by slug"""
@ -204,7 +202,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Set application icon"""
app: Application = self.get_object()
icon = request.FILES.get("file", None)
clear = request.data.get("clear", False)
clear = request.data.get("clear", "false").lower() == "true"
if clear:
# .delete() saves the model by default
app.meta_icon.delete()

View File

@ -3,23 +3,33 @@ from traceback import format_tb
from typing import Optional
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.lib.expression.evaluator import BaseEvaluator
from authentik.policies.types import PolicyRequest
class PropertyMappingEvaluator(BaseEvaluator):
"""Custom Evalautor that adds some different context variables."""
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"""
req = PolicyRequest(user=get_anonymous_user())
req.obj = mapping
if user:
req.user = user
self._context["user"] = user
if request:
self._context["request"] = request
req.http_request = request
self._context["request"] = req
self._context.update(**kwargs)
def handle_error(self, exc: Exception, expression_source: str):
@ -30,9 +40,8 @@ class PropertyMappingEvaluator(BaseEvaluator):
expression=expression_source,
message=error_string,
)
if "user" in self._context:
event.set_user(self._context["user"])
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
event.save()

View File

@ -26,6 +26,8 @@ class ImpersonateMiddleware:
if SESSION_IMPERSONATE_USER in request.session:
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)

View File

@ -6,6 +6,7 @@ from urllib.parse import urlencode
from uuid import uuid4
import django.db.models.options as options
from deepmerge import always_merger
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager
@ -114,8 +115,8 @@ class User(GuardianUserMixin, AbstractUser):
including the users attributes"""
final_attributes = {}
for group in self.ak_groups.all().order_by("name"):
final_attributes.update(group.attributes)
final_attributes.update(self.attributes)
always_merger.merge(final_attributes, group.attributes)
always_merger.merge(final_attributes, self.attributes)
return final_attributes
@cached_property
@ -142,21 +143,25 @@ class User(GuardianUserMixin, AbstractUser):
@property
def avatar(self) -> str:
"""Get avatar, depending on authentik.avatar setting"""
mode = CONFIG.raw.get("authentik").get("avatars")
mode: str = CONFIG.y("avatars", "none")
if mode == "none":
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":
parameters = [
("s", "158"),
("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 = (
f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}"
)
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:
@ -460,7 +465,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
from authentik.core.expression import PropertyMappingEvaluator
evaluator = PropertyMappingEvaluator()
evaluator.set_context(user, request, **kwargs)
evaluator.set_context(user, request, self, **kwargs)
try:
return evaluator.evaluate(self.expression)
except (ValueError, SyntaxError) as exc:
@ -494,8 +499,12 @@ class AuthenticatedSession(ExpiringModel):
last_used = models.DateTimeField(auto_now=True)
@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"""
if not hasattr(request, "session") or not request.session.session_key:
return None
return AuthenticatedSession(
session_key=request.session.session_key,
user=user,

View File

@ -1,11 +1,12 @@
"""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.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.core.signals import Signal
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.http.request import HttpRequest
from prometheus_client import Gauge
@ -18,7 +19,7 @@ GAUGE_MODELS = Gauge(
)
if TYPE_CHECKING:
from authentik.core.models import User
from authentik.core.models import AuthenticatedSession, User
@receiver(post_save)
@ -48,7 +49,9 @@ def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
"""Create an AuthenticatedSession from request"""
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)
@ -60,3 +63,17 @@ def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
AuthenticatedSession.objects.filter(
session_key=request.session.session_key
).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.lib.utils.urls import redirect_with_qs
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.prompt.stage import PLAN_CONTEXT_PROMPT
@ -182,6 +183,8 @@ class SourceFlowManager:
# pylint: disable=unused-argument
def get_stages_to_append(self, flow: Flow) -> list[Stage]:
"""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:
return [
in_memory_stage(PostUserEnrollmentStage),
@ -198,7 +201,7 @@ class SourceFlowManager:
kwargs.update(
{
# 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_SOURCE: self.source,
PLAN_CONTEXT_REDIRECT: final_redirect,
@ -210,7 +213,7 @@ class SourceFlowManager:
planner = FlowPlanner(flow)
plan = planner.plan(self.request, kwargs)
for stage in self.get_stages_to_append(flow):
plan.append(stage)
plan.append_stage(stage=stage)
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",

View File

@ -3,16 +3,6 @@
{% load static %}
{% 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 %}
{% trans 'End session' %} - {{ tenant.branding_title }}
{% endblock %}

View File

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

View File

@ -7,6 +7,14 @@
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}?v={{ ak_version }}">
{% endblock %}
{% block head %}
<style>
.pf-c-background-image::before {
--ak-flow-background: url("/static/dist/assets/images/flow_background.jpg");
}
</style>
{% endblock %}
{% block body %}
<div class="pf-c-background-image">
<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):
"""Test check_access operation"""
self.client.force_login(self.user)
response = self.client.post(
response = self.client.get(
reverse(
"authentik_api:application-check-access",
kwargs={"slug": self.allowed.slug},
@ -36,7 +36,7 @@ class TestApplicationsAPI(APITestCase):
self.assertJSONEqual(
force_str(response.content), {"messages": [], "passing": True}
)
response = self.client.post(
response = self.client.get(
reverse(
"authentik_api:application-check-access",
kwargs={"slug": self.denied.slug},

View File

@ -17,6 +17,9 @@ class TestImpersonation(TestCase):
def test_impersonate_simple(self):
"""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.get(

View File

@ -2,7 +2,7 @@
from dataclasses import dataclass
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.flows.challenge import Challenge
@ -22,18 +22,10 @@ class UILoginButton:
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):
"""Serializer for User settings for stages and sources"""
object_uid = CharField()
component = CharField()
title = CharField()
configure_url = CharField()
configure_url = CharField(required=False)

View File

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

View File

@ -6,11 +6,11 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import CharField, DictField, IntegerField
from rest_framework.fields import DictField, IntegerField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer
from authentik.events.models import Event, EventAction
@ -19,11 +19,6 @@ from authentik.events.models import Event, EventAction
class EventSerializer(ModelSerializer):
"""Event Serializer"""
# Since we only use this serializer for read-only operations,
# no checking of the action is done here.
# This allows clients to check wildcards, prefixes and custom types
action = CharField()
class Meta:
model = Event
@ -36,6 +31,7 @@ class EventSerializer(ModelSerializer):
"client_ip",
"created",
"expires",
"tenant",
]
@ -76,6 +72,11 @@ class EventsFilter(django_filters.FilterSet):
field_name="action",
lookup_expr="icontains",
)
tenant_name = django_filters.CharFilter(
field_name="tenant",
lookup_expr="name",
label="Tenant name",
)
# pylint: disable=unused-argument
def filter_context_model_pk(self, queryset, name, value):
@ -90,7 +91,7 @@ class EventsFilter(django_filters.FilterSet):
fields = ["action", "client_ip", "username"]
class EventViewSet(ReadOnlyModelViewSet):
class EventViewSet(ModelViewSet):
"""Event Read-Only Viewset"""
queryset = Event.objects.all()

View File

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

View File

@ -2,6 +2,7 @@
from functools import partial
from typing import Callable
from django.conf import settings
from django.db.models import Model
from django.db.models.signals import post_save, pre_delete
from django.http import HttpRequest, HttpResponse
@ -12,6 +13,8 @@ from authentik.core.models import User
from authentik.events.models import Event, EventAction, Notification
from authentik.events.signals import EventNewThread
from authentik.events.utils import model_to_dict
from authentik.lib.sentry import before_send
from authentik.lib.utils.errors import exception_to_string
class AuditMiddleware:
@ -54,10 +57,20 @@ class AuditMiddleware:
# pylint: disable=unused-argument
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"])
pre_delete.disconnect(dispatch_uid=LOCAL.authentik["request_id"])
if settings.DEBUG:
return
if before_send({}, {"exc_info": (None, exception, None)}) is not None:
thread = EventNewThread(
EventAction.SYSTEM_EXCEPTION,
request,
message=exception_to_string(exception),
)
thread.run()
@staticmethod
# pylint: disable=unused-argument
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.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.utils.http import get_client_ip
from authentik.policies.models import PolicyBindingModel
from authentik.stages.email.utils import TemplateEmailMessage
from authentik.tenants.utils import DEFAULT_TENANT
LOGGER = get_logger("authentik.events")
GAUGE_EVENTS = Gauge(
@ -40,6 +41,11 @@ def default_event_duration():
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):
"""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_EXCEPTION = "system_task_exception"
SYSTEM_EXCEPTION = "system_exception"
CONFIGURATION_ERROR = "configuration_error"
@ -94,6 +101,7 @@ class Event(ExpiringModel):
context = models.JSONField(default=dict, blank=True)
client_ip = models.GenericIPAddressField(null=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
expires = models.DateTimeField(default=default_event_duration)
@ -132,6 +140,13 @@ class Event(ExpiringModel):
"""Add data from a Django-HttpRequest, allowing the creation of
Events independently 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"):
original_user = None
if hasattr(request, "session"):

View File

@ -105,7 +105,11 @@ def notification_transport(
"""Send notification over specified transport"""
self.save_on_success = False
try:
notification: Notification = Notification.objects.get(pk=notification_pk)
notification: Notification = Notification.objects.filter(
pk=notification_pk
).first()
if not notification:
return
transport: NotificationTransport = NotificationTransport.objects.get(
pk=transport_pk
)

View File

@ -25,6 +25,7 @@ class FlowStageBindingSerializer(ModelSerializer):
"re_evaluate_policies",
"order",
"policy_engine_mode",
"invalid_response_action",
]

View File

@ -301,10 +301,14 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
"""Set Flow background"""
flow: Flow = self.get_object()
background = request.FILES.get("file", None)
clear = request.data.get("clear", False)
clear = request.data.get("clear", "false").lower() == "true"
if clear:
# .delete() saves the model by default
flow.background.delete()
if flow.background_url.startswith("/media"):
# .delete() saves the model by default
flow.background.delete()
else:
flow.background = None
flow.save()
return Response({})
if background:
flow.background = background

View File

@ -93,7 +93,7 @@ class StageViewSet(
if not user_settings:
continue
user_settings.initial_data["object_uid"] = str(stage.pk)
if hasattr(stage, "configure_flow"):
if hasattr(stage, "configure_flow") and stage.configure_flow:
user_settings.initial_data["configure_url"] = reverse(
"authentik_flows:configure",
kwargs={"stage_uuid": stage.pk},

View File

@ -5,8 +5,7 @@ from typing import TYPE_CHECKING, Optional
from django.http.request import HttpRequest
from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.flows.models import Stage
from authentik.flows.models import FlowStageBinding
from authentik.policies.engine import PolicyEngine
from authentik.policies.models import PolicyBinding
@ -22,11 +21,14 @@ class StageMarker:
# pylint: disable=unused-argument
def process(
self, plan: "FlowPlan", stage: Stage, http_request: Optional[HttpRequest]
) -> Optional[Stage]:
self,
plan: "FlowPlan",
binding: FlowStageBinding,
http_request: HttpRequest,
) -> Optional[FlowStageBinding]:
"""Process callback for this marker. This should be overridden by sub-classes.
If a stage should be removed, return None."""
return stage
return binding
@dataclass
@ -34,24 +36,34 @@ class ReevaluateMarker(StageMarker):
"""Reevaluate Marker, forces stage's policies to be evaluated again."""
binding: PolicyBinding
user: User
def process(
self, plan: "FlowPlan", stage: Stage, http_request: Optional[HttpRequest]
) -> Optional[Stage]:
self,
plan: "FlowPlan",
binding: FlowStageBinding,
http_request: HttpRequest,
) -> Optional[FlowStageBinding]:
"""Re-evaluate policies bound to stage, and if they fail, remove from plan"""
engine = PolicyEngine(self.binding, self.user)
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
LOGGER.debug(
"f(plan_inst)[re-eval marker]: running re-evaluation",
binding=binding,
policy_binding=self.binding,
)
engine = PolicyEngine(
self.binding, plan.context.get(PLAN_CONTEXT_PENDING_USER, http_request.user)
)
engine.use_cache = False
if http_request:
engine.request.set_http_request(http_request)
engine.request.set_http_request(http_request)
engine.request.context = plan.context
engine.build()
result = engine.result
if result.passing:
return stage
return binding
LOGGER.warning(
"f(plan_inst)[re-eval marker]: stage failed re-evaluation",
stage=stage,
"f(plan_inst)[re-eval marker]: binding failed re-evaluation",
binding=binding,
messages=result.messages,
)
return None

View File

@ -6,6 +6,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.flows.models import FlowDesignation
from authentik.stages.identification.models import UserFields
from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP
def create_default_authentication_flow(
@ -31,7 +32,7 @@ def create_default_authentication_flow(
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
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(

View File

@ -15,9 +15,6 @@ PREFILL_POLICY_EXPRESSION = """# This policy sets the user for the currently run
# by injecting "pending_user"
akadmin = ak_user_by(username="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"""
@ -138,7 +135,7 @@ class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0017_auto_20210329_1334"),
("authentik_stages_user_write", "__latest__"),
("authentik_stages_user_write", "0002_auto_20200918_1653"),
("authentik_stages_user_login", "__latest__"),
("authentik_stages_password", "0002_passwordstage_change_flow"),
("authentik_policies", "0001_initial"),

View File

@ -0,0 +1,22 @@
# Generated by Django 3.2.4 on 2021-06-27 16:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0020_flow_compatibility_mode"),
]
operations = [
migrations.AddField(
model_name="flowstagebinding",
name="invalid_response_action",
field=models.TextField(
choices=[("retry", "Retry"), ("continue", "Continue")],
default="retry",
help_text="Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor while CONTINUE continues with the next stage.",
),
),
]

View File

@ -27,6 +27,14 @@ class NotConfiguredAction(models.TextChoices):
CONFIGURE = "configure"
class InvalidResponseAction(models.TextChoices):
"""Configure how the flow executor should handle invalid responses to challenges"""
RETRY = "retry"
RESTART = "restart"
RESTART_WITH_CONTEXT = "restart_with_context"
class FlowDesignation(models.TextChoices):
"""Designation of what a Flow should be used for. At a later point, this
should be replaced by a database entry."""
@ -201,6 +209,17 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
help_text=_("Evaluate policies when the Stage is present to the user."),
)
invalid_response_action = models.TextField(
choices=InvalidResponseAction.choices,
default=InvalidResponseAction.RETRY,
help_text=_(
"Configure how the flow executor should handle an invalid response to a "
"challenge. RETRY returns the error message and a similar challenge to the "
"executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT "
"restarts the flow while keeping the current context."
),
)
order = models.IntegerField()
objects = InheritanceManager()

View File

@ -14,6 +14,7 @@ from authentik.events.models import cleanse_dict
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import Flow, FlowStageBinding, Stage
from authentik.lib.config import CONFIG
from authentik.policies.engine import PolicyEngine
from authentik.root.monitoring import UpdatingGauge
@ -33,6 +34,7 @@ HIST_FLOWS_PLAN_TIME = Histogram(
"Duration to build a plan for a flow",
["flow_slug"],
)
CACHE_TIMEOUT = int(CONFIG.y("redis.cache_timeout_flows"))
def cache_key(flow: Flow, user: Optional[User] = None) -> str:
@ -50,33 +52,41 @@ class FlowPlan:
flow_pk: str
stages: list[Stage] = field(default_factory=list)
bindings: list[FlowStageBinding] = field(default_factory=list)
context: dict[str, Any] = field(default_factory=dict)
markers: list[StageMarker] = field(default_factory=list)
def append(self, stage: Stage, marker: Optional[StageMarker] = None):
def append_stage(self, stage: Stage, marker: Optional[StageMarker] = None):
"""Append `stage` to all stages, optionall with stage marker"""
self.stages.append(stage)
return self.append(FlowStageBinding(stage=stage), marker)
def append(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None):
"""Append `stage` to all stages, optionall with stage marker"""
self.bindings.append(binding)
self.markers.append(marker or StageMarker())
def insert(self, stage: Stage, marker: Optional[StageMarker] = None):
def insert_stage(self, stage: Stage, marker: Optional[StageMarker] = None):
"""Insert stage into plan, as immediate next stage"""
self.stages.insert(1, stage)
self.bindings.insert(1, FlowStageBinding(stage=stage, order=0))
self.markers.insert(1, marker or StageMarker())
def next(self, http_request: Optional[HttpRequest]) -> Optional[Stage]:
def next(self, http_request: Optional[HttpRequest]) -> Optional[FlowStageBinding]:
"""Return next pending stage from the bottom of the list"""
if not self.has_stages:
return None
stage = self.stages[0]
binding = self.bindings[0]
marker = self.markers[0]
if marker.__class__ is not StageMarker:
LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker)
marked_stage = marker.process(self, stage, http_request)
LOGGER.debug(
"f(plan_inst): stage has marker", binding=binding, marker=marker
)
marked_stage = marker.process(self, binding, http_request)
if not marked_stage:
LOGGER.debug("f(plan_inst): marker returned none, next stage", stage=stage)
self.stages.remove(stage)
LOGGER.debug(
"f(plan_inst): marker returned none, next stage", binding=binding
)
self.bindings.remove(binding)
self.markers.remove(marker)
if not self.has_stages:
return None
@ -87,12 +97,12 @@ class FlowPlan:
def pop(self):
"""Pop next pending stage from bottom of list"""
self.markers.pop(0)
self.stages.pop(0)
self.bindings.pop(0)
@property
def has_stages(self) -> bool:
"""Check if there are any stages left in this plan"""
return len(self.markers) + len(self.stages) > 0
return len(self.markers) + len(self.bindings) > 0
class FlowPlanner:
@ -157,9 +167,9 @@ class FlowPlanner:
"f(plan): building plan",
)
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()
if not plan.stages and not self.allow_empty_flows:
if not plan.bindings and not self.allow_empty_flows:
raise EmptyFlowException()
return plan
@ -214,9 +224,9 @@ class FlowPlanner:
"f(plan): stage has re-evaluate marker",
stage=binding.stage,
)
marker = ReevaluateMarker(binding=binding, user=user)
marker = ReevaluateMarker(binding=binding)
if stage:
plan.append(stage, marker)
plan.append(binding, marker)
HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug)
self._logger.debug(
"f(plan): finished building",

View File

@ -16,29 +16,14 @@ from authentik.flows.challenge import (
HttpChallengeResponse,
WithUserInfoChallenge,
)
from authentik.flows.models import InvalidResponseAction
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.views import FlowExecutorView
from authentik.lib.sentry import SentryIgnoredException
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
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):
"""Abstract Stage, inherits TemplateView but can be combined with FormView"""
@ -85,7 +70,13 @@ class ChallengeStageView(StageView):
"""Return a challenge for the frontend to solve"""
challenge = self._get_challenge(*args, **kwargs)
if not challenge.is_valid():
LOGGER.warning(challenge.errors, stage_view=self, challenge=challenge)
LOGGER.warning(
"f(ch): Invalid challenge",
binding=self.executor.current_binding,
errors=challenge.errors,
stage_view=self,
challenge=challenge,
)
return HttpChallengeResponse(challenge)
# pylint: disable=unused-argument
@ -93,6 +84,21 @@ class ChallengeStageView(StageView):
"""Handle challenge response"""
challenge: ChallengeResponse = self.get_response_instance(data=request.data)
if not challenge.is_valid():
if self.executor.current_binding.invalid_response_action in [
InvalidResponseAction.RESTART,
InvalidResponseAction.RESTART_WITH_CONTEXT,
]:
keep_context = (
self.executor.current_binding.invalid_response_action
== InvalidResponseAction.RESTART_WITH_CONTEXT
)
LOGGER.debug(
"f(ch): Invalid response, restarting flow",
binding=self.executor.current_binding,
stage_view=self,
keep_context=keep_context,
)
return self.executor.restart_flow(keep_context)
return self.challenge_invalid(challenge)
return self.challenge_valid(challenge)
@ -142,5 +148,10 @@ class ChallengeStageView(StageView):
)
challenge_response.initial_data["response_errors"] = full_errors
if not challenge_response.is_valid():
LOGGER.warning(challenge_response.errors)
LOGGER.warning(
"f(ch): invalid challenge response",
binding=self.executor.current_binding,
errors=challenge_response.errors,
stage_view=self,
)
return HttpChallengeResponse(challenge_response)

View File

@ -182,8 +182,8 @@ class TestFlowPlanner(TestCase):
planner = FlowPlanner(flow)
plan = planner.plan(request)
self.assertEqual(plan.stages[0], binding.stage)
self.assertEqual(plan.stages[1], binding2.stage)
self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.bindings[1], binding2)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)

View File

@ -11,15 +11,23 @@ from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.models import (
Flow,
FlowDesignation,
FlowStageBinding,
InvalidResponseAction,
)
from authentik.flows.planner import FlowPlan, FlowPlanner
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView
from authentik.lib.config import CONFIG
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
from authentik.policies.reputation.models import ReputationPolicy
from authentik.policies.types import PolicyResult
from authentik.stages.deny.models import DenyStage
from authentik.stages.dummy.models import DummyStage
from authentik.stages.identification.models import IdentificationStage, UserFields
POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False))
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
@ -52,8 +60,9 @@ class TestFlowExecutor(TestCase):
designation=FlowDesignation.AUTHENTICATION,
)
stage = DummyStage.objects.create(name="dummy")
binding = FlowStageBinding(target=flow, stage=stage, order=0)
plan = FlowPlan(
flow_pk=flow.pk.hex + "a", stages=[stage], markers=[StageMarker()]
flow_pk=flow.pk.hex + "a", bindings=[binding], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
@ -163,7 +172,7 @@ class TestFlowExecutor(TestCase):
# Check that two stages are in plan
session = self.client.session
plan: FlowPlan = session[SESSION_KEY_PLAN]
self.assertEqual(len(plan.stages), 2)
self.assertEqual(len(plan.bindings), 2)
# Second request, submit form, one stage left
response = self.client.post(exec_url)
# Second request redirects to the same URL
@ -172,7 +181,7 @@ class TestFlowExecutor(TestCase):
# Check that two stages are in plan
session = self.client.session
plan: FlowPlan = session[SESSION_KEY_PLAN]
self.assertEqual(len(plan.stages), 1)
self.assertEqual(len(plan.bindings), 1)
@patch(
"authentik.flows.views.to_stage_response",
@ -213,8 +222,8 @@ class TestFlowExecutor(TestCase):
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding.stage)
self.assertEqual(plan.stages[1], binding2.stage)
self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.bindings[1], binding2)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
@ -267,9 +276,9 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200)
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding.stage)
self.assertEqual(plan.stages[1], binding2.stage)
self.assertEqual(plan.stages[2], binding3.stage)
self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.bindings[2], binding3)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
@ -281,8 +290,8 @@ class TestFlowExecutor(TestCase):
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding2.stage)
self.assertEqual(plan.stages[1], binding3.stage)
self.assertEqual(plan.bindings[0], binding2)
self.assertEqual(plan.bindings[1], binding3)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], StageMarker)
@ -338,9 +347,9 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200)
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding.stage)
self.assertEqual(plan.stages[1], binding2.stage)
self.assertEqual(plan.stages[2], binding3.stage)
self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.bindings[2], binding3)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
@ -352,8 +361,8 @@ class TestFlowExecutor(TestCase):
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding2.stage)
self.assertEqual(plan.stages[1], binding3.stage)
self.assertEqual(plan.bindings[0], binding2)
self.assertEqual(plan.bindings[1], binding3)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], StageMarker)
@ -364,7 +373,7 @@ class TestFlowExecutor(TestCase):
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding3.stage)
self.assertEqual(plan.bindings[0], binding3)
self.assertIsInstance(plan.markers[0], StageMarker)
@ -438,10 +447,10 @@ class TestFlowExecutor(TestCase):
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding.stage)
self.assertEqual(plan.stages[1], binding2.stage)
self.assertEqual(plan.stages[2], binding3.stage)
self.assertEqual(plan.stages[3], binding4.stage)
self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.bindings[2], binding3)
self.assertEqual(plan.bindings[3], binding4)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
@ -512,3 +521,78 @@ class TestFlowExecutor(TestCase):
stage_view = StageView(executor)
self.assertEqual(ident, stage_view.get_pending_user(for_display=True).username)
def test_invalid_restart(self):
"""Test flow that restarts on invalid entry"""
flow = Flow.objects.create(
name="restart-on-invalid",
slug="restart-on-invalid",
designation=FlowDesignation.AUTHENTICATION,
)
# Stage 0 is a deny stage that is added dynamically
# when the reputation policy says so
deny_stage = DenyStage.objects.create(name="deny")
reputation_policy = ReputationPolicy.objects.create(
name="reputation", threshold=-1, check_ip=False
)
deny_binding = FlowStageBinding.objects.create(
target=flow,
stage=deny_stage,
order=0,
evaluate_on_plan=False,
re_evaluate_policies=True,
)
PolicyBinding.objects.create(
policy=reputation_policy, target=deny_binding, order=0
)
# Stage 1 is an identification stage
ident_stage = IdentificationStage.objects.create(
name="ident",
user_fields=[UserFields.E_MAIL],
)
FlowStageBinding.objects.create(
target=flow,
stage=ident_stage,
order=1,
invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT,
)
exec_url = reverse(
"authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}
)
# First request, run the planner
response = self.client.get(exec_url)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-identification",
"flow_info": {
"background": flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
"title": "",
},
"password_fields": False,
"primary_action": "Log in",
"sources": [],
"user_fields": [UserFields.E_MAIL],
},
)
response = self.client.post(
exec_url, {"uid_field": "invalid-string"}, follow=True
)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{
"component": "ak-stage-access-denied",
"error_message": None,
"flow_info": {
"background": flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
"title": "",
},
"type": ChallengeTypes.NATIVE.value,
},
)

View File

@ -4,6 +4,7 @@ from typing import Any, Optional
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.cache import cache
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect
from django.http.request import QueryDict
from django.shortcuts import get_object_or_404, redirect
@ -37,13 +38,20 @@ from authentik.flows.challenge import (
WithUserInfoChallenge,
)
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
from authentik.flows.models import (
ConfigurableStage,
Flow,
FlowDesignation,
FlowStageBinding,
Stage,
)
from authentik.flows.planner import (
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_REDIRECT,
FlowPlan,
FlowPlanner,
)
from authentik.lib.sentry import SentryIgnoredException
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.tenants.models import Tenant
@ -93,6 +101,10 @@ def challenge_response_types():
return Inner()
class InvalidStageError(SentryIgnoredException):
"""Error raised when a challenge from a stage is not valid"""
@method_decorator(xframe_options_sameorigin, name="dispatch")
class FlowExecutorView(APIView):
"""Stage 1 Flow executor, passing requests to Stage Views"""
@ -102,6 +114,7 @@ class FlowExecutorView(APIView):
flow: Flow
plan: Optional[FlowPlan] = None
current_binding: FlowStageBinding
current_stage: Stage
current_stage_view: View
@ -154,27 +167,35 @@ class FlowExecutorView(APIView):
request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", ""))
# We don't save the Plan after getting the next stage
# as it hasn't been successfully passed yet
next_stage = self.plan.next(self.request)
if not next_stage:
next_binding = self.plan.next(self.request)
if not next_binding:
self._logger.debug("f(exec): no more stages, flow is done.")
return self._flow_done()
self.current_stage = next_stage
self.current_binding = next_binding
self.current_stage = next_binding.stage
self._logger.debug(
"f(exec): Current stage",
current_stage=self.current_stage,
flow_slug=self.flow.slug,
)
stage_cls = self.current_stage.type
try:
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.args = self.args
self.current_stage_view.kwargs = self.kwargs
self.current_stage_view.request = request
return super().dispatch(request)
try:
return super().dispatch(request)
except InvalidStageError as exc:
return self.stage_invalid(str(exc))
@extend_schema(
responses={
200: PolymorphicProxySerializer(
component_name="FlowChallengeRequest",
component_name="ChallengeTypes",
serializers=challenge_types(),
resource_type_field_name="component",
),
@ -214,7 +235,7 @@ class FlowExecutorView(APIView):
@extend_schema(
responses={
200: PolymorphicProxySerializer(
component_name="FlowChallengeRequest",
component_name="ChallengeTypes",
serializers=challenge_types(),
resource_type_field_name="component",
),
@ -256,8 +277,31 @@ class FlowExecutorView(APIView):
planner = FlowPlanner(self.flow)
plan = planner.plan(self.request)
self.request.session[SESSION_KEY_PLAN] = plan
try:
# Call the has_stages getter to check that
# there are no issues with the class we might've gotten
# from the cache. If there are errors, just delete all cached flows
_ = plan.has_stages
except Exception: # pylint: disable=broad-except
keys = cache.keys("flow_*")
cache.delete_many(keys)
return self._initiate_plan()
return plan
def restart_flow(self, keep_context=False) -> HttpResponse:
"""Restart the currently active flow, optionally keeping the current context"""
planner = FlowPlanner(self.flow)
default_context = None
if keep_context:
default_context = self.plan.context
plan = planner.plan(self.request, default_context)
self.request.session[SESSION_KEY_PLAN] = plan
kwargs = self.kwargs
kwargs.update({"flow_slug": self.flow.slug})
return redirect_with_qs(
"authentik_api:flow-executor", self.request.GET, **kwargs
)
def _flow_done(self) -> HttpResponse:
"""User Successfully passed all stages"""
# Since this is wrapped by the ExecutorShell, the next argument is saved in the session
@ -281,10 +325,10 @@ class FlowExecutorView(APIView):
)
self.plan.pop()
self.request.session[SESSION_KEY_PLAN] = self.plan
if self.plan.stages:
if self.plan.bindings:
self._logger.debug(
"f(exec): Continuing with next stage",
remaining=len(self.plan.stages),
remaining=len(self.plan.bindings),
)
kwargs = self.kwargs
kwargs.update({"flow_slug": self.flow.slug})
@ -353,8 +397,11 @@ class FlowErrorResponse(TemplateResponse):
context = {}
context["error"] = self.error
if self._request.user and self._request.user.is_authenticated:
if self._request.user.is_superuser or self._request.user.attributes.get(
USER_ATTRIBUTE_DEBUG, False
if (
self._request.user.is_superuser
or self._request.user.group_attributes().get(
USER_ATTRIBUTE_DEBUG, False
)
):
context["tb"] = "".join(format_tb(self.error.__traceback__))
return context

View File

@ -62,7 +62,7 @@ class ConfigLoader:
output.update(kwargs)
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"""
for key, value in updatee.items():
if isinstance(value, Mapping):
@ -73,7 +73,7 @@ class ConfigLoader:
root[key] = value
return root
def parse_uri(self, value):
def parse_uri(self, value: str) -> str:
"""Parse string values which start with a URI"""
url = urlparse(value)
if url.scheme == "env":
@ -99,7 +99,10 @@ class ConfigLoader:
raise ImproperlyConfigured from exc
except PermissionError as exc:
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):

View File

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

View File

@ -3,6 +3,7 @@ import re
from textwrap import indent
from typing import Any, Iterable, Optional
from django.core.exceptions import FieldError
from requests import Session
from rest_framework.serializers import ValidationError
from sentry_sdk.hub import Hub
@ -29,10 +30,10 @@ class BaseEvaluator:
# update website/docs/expressions/_objects.md
# update website/docs/expressions/_functions.md
self._globals = {
"regex_match": BaseEvaluator.expr_filter_regex_match,
"regex_replace": BaseEvaluator.expr_filter_regex_replace,
"ak_is_group_member": BaseEvaluator.expr_func_is_group_member,
"ak_user_by": BaseEvaluator.expr_func_user_by,
"regex_match": BaseEvaluator.expr_regex_match,
"regex_replace": BaseEvaluator.expr_regex_replace,
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
"ak_user_by": BaseEvaluator.expr_user_by,
"ak_logger": get_logger(),
"requests": Session(),
}
@ -40,25 +41,28 @@ class BaseEvaluator:
self._filename = "BaseEvalautor"
@staticmethod
def expr_filter_regex_match(value: Any, regex: str) -> bool:
def expr_regex_match(value: Any, regex: str) -> bool:
"""Expression Filter to run re.search"""
return re.search(regex, value) is None
return re.search(regex, value) is not None
@staticmethod
def expr_filter_regex_replace(value: Any, regex: str, repl: str) -> str:
def expr_regex_replace(value: Any, regex: str, repl: str) -> str:
"""Expression Filter to run re.sub"""
return re.sub(regex, repl, value)
@staticmethod
def expr_func_user_by(**filters) -> Optional[User]:
def expr_user_by(**filters) -> Optional[User]:
"""Get user by filters"""
users = User.objects.filter(**filters)
if users:
return users.first()
return None
try:
users = User.objects.filter(**filters)
if users:
return users.first()
return None
except FieldError:
return None
@staticmethod
def expr_func_is_group_member(user: User, **group_filters) -> bool:
def expr_is_group_member(user: User, **group_filters) -> bool:
"""Check if `user` is member of group with name `group_name`"""
return user.ak_groups.filter(**group_filters).exists()

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,32 @@
"""Test Evaluator base functions"""
from django.test import TestCase
from authentik.core.models import User
from authentik.lib.expression.evaluator import BaseEvaluator
class TestEvaluator(TestCase):
"""Test Evaluator base functions"""
def test_regex_match(self):
"""Test expr_regex_match"""
self.assertFalse(BaseEvaluator.expr_regex_match("foo", "bar"))
self.assertTrue(BaseEvaluator.expr_regex_match("foo", "foo"))
def test_regex_replace(self):
"""Test expr_regex_replace"""
self.assertEqual(BaseEvaluator.expr_regex_replace("foo", "o", "a"), "faa")
def test_user_by(self):
"""Test expr_user_by"""
self.assertIsNotNone(BaseEvaluator.expr_user_by(username="akadmin"))
self.assertIsNone(BaseEvaluator.expr_user_by(username="bar"))
self.assertIsNone(BaseEvaluator.expr_user_by(foo="bar"))
def test_is_group_member(self):
"""Test expr_is_group_member"""
self.assertFalse(
BaseEvaluator.expr_is_group_member(
User.objects.get(username="akadmin"), name="test"
)
)

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
if OUTPOST_REMOTE_IP_HEADER not in request.META:
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 request.META[OUTPOST_REMOTE_IP_HEADER]

View File

@ -33,6 +33,13 @@ class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
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:
model = OutpostServiceConnection

View File

@ -67,11 +67,6 @@ class OutpostConsumer(AuthJsonConsumer):
self.accept()
self.outpost = outpost.first()
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
def disconnect(self, close_code):
@ -108,6 +103,11 @@ class OutpostConsumer(AuthJsonConsumer):
outpost=self.outpost.name,
uid=self.last_uid,
).inc()
LOGGER.debug(
"added outpost instace to cache",
outpost=self.outpost,
instance_uuid=self.last_uid,
)
self.first_msg = True
if msg.instruction == WebsocketMessageInstruction.HELLO:

View File

@ -53,6 +53,27 @@ class DockerController(BaseController):
return True
return False
def _comp_ports(self, container: Container) -> bool:
"""Check that the container has the correct ports exposed. Return true if container needs
to be rebuilt."""
# with TEST enabled, we use host-network
if settings.TEST:
return False
# When the container isn't running, the API doesn't report any port mappings
if container.status != "running":
return False
# {'6379/tcp': [{'HostIp': '127.0.0.1', 'HostPort': '6379'}]}
for port in self.deployment_ports:
key = f"{port.inner_port or port.port}/{port.protocol.lower()}"
if key not in container.ports:
return True
host_matching = False
for host_port in container.ports[key]:
host_matching = host_port.get("HostPort") == port.port
if not host_matching:
return True
return False
def _get_container(self) -> tuple[Container, bool]:
container_name = f"authentik-proxy-{self.outpost.uuid.hex}"
try:
@ -63,10 +84,10 @@ class DockerController(BaseController):
self.client.images.pull(image_name)
container_args = {
"image": image_name,
"name": f"authentik-proxy-{self.outpost.uuid.hex}",
"name": container_name,
"detach": True,
"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
},
"environment": self._get_env(),
@ -98,6 +119,11 @@ class DockerController(BaseController):
)
self.down()
return self.up()
# Check container's ports
if self._comp_ports(container):
self.logger.info("Container has mis-matched ports, re-creating...")
self.down()
return self.up()
# Check that container values match our values
if self._comp_env(container):
self.logger.info("Container has outdated config, re-creating...")

View File

@ -8,7 +8,7 @@ from uuid import uuid4
from dacite import from_dict
from django.contrib.auth.models import Permission
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.utils.translation import gettext_lazy as _
from docker.client import DockerClient
@ -50,6 +50,8 @@ class ServiceConnectionInvalid(SentryIgnoredException):
class OutpostConfig:
"""Configuration an outpost uses to configure it self"""
# update website/docs/outposts/outposts.md
authentik_host: str
authentik_host_insecure: bool = False
@ -141,7 +143,9 @@ class OutpostServiceConnection(models.Model):
@property
def component(self) -> str:
"""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:
@ -380,25 +384,31 @@ class Outpost(models.Model):
tokens = Token.filter_not_expired(
identifier=self.token_identifier,
intent=TokenIntents.INTENT_API,
)
if tokens.exists():
token = tokens.first()
if not token.managed:
token.managed = managed
token.save()
return token
return Token.objects.create(
user=self.user,
identifier=self.token_identifier,
intent=TokenIntents.INTENT_API,
description=f"Autogenerated by authentik for Outpost {self.name}",
expiring=False,
managed=managed,
)
if tokens.exists():
return tokens.first()
try:
return Token.objects.create(
user=self.user,
identifier=self.token_identifier,
intent=TokenIntents.INTENT_API,
description=f"Autogenerated by authentik for Outpost {self.name}",
expiring=False,
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]]:
"""Get an iterator of all objects the user needs read access to"""
objects: list[Union[models.Model, str]] = [self]
objects: list[Union[models.Model, str]] = [
self,
"authentik_events.add_event",
]
for provider in (
Provider.objects.filter(outpost=self).select_related().select_subclasses()
):

View File

@ -37,7 +37,9 @@ class AccessDeniedResponse(TemplateResponse):
if self._request.user and self._request.user.is_authenticated:
if (
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
return context

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,7 +1,6 @@
"""authentik policy task"""
from multiprocessing import get_context
from multiprocessing.connection import Connection
from traceback import format_tb
from typing import Optional
from django.core.cache import cache
@ -11,14 +10,16 @@ from sentry_sdk.tracing import Span
from structlog.stdlib import get_logger
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.models import PolicyBinding
from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger()
TRACEBACK_HEADER = "Traceback (most recent call last):\n"
FORK_CTX = get_context("fork")
CACHE_TIMEOUT = int(CONFIG.y("redis.cache_timeout_policies"))
PROCESS_CLASS = FORK_CTX.Process
HIST_POLICIES_EXECUTION_TIME = Histogram(
"authentik_policies_execution_time",
@ -106,11 +107,7 @@ class PolicyProcess(PROCESS_CLASS):
except PolicyException as exc:
# Either use passed original exception or whatever we have
src_exc = exc.src_exc if exc.src_exc else exc
error_string = (
TRACEBACK_HEADER
+ "".join(format_tb(src_exc.__traceback__))
+ str(src_exc)
)
error_string = exception_to_string(src_exc)
# Create policy exception event, only when we're not debugging
if not self.request.debug:
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
@ -119,7 +116,7 @@ class PolicyProcess(PROCESS_CLASS):
policy_result.source_binding = self.binding
if not self.request.debug:
key = cache_key(self.binding, self.request)
cache.set(key, policy_result)
cache.set(key, policy_result, CACHE_TIMEOUT)
LOGGER.debug(
"P_ENG(proc): finished and cached ",
policy=self.binding.policy,

View File

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

View File

@ -5,6 +5,7 @@ from django.dispatch import receiver
from django.http import HttpRequest
from structlog.stdlib import get_logger
from authentik.lib.config import CONFIG
from authentik.lib.utils.http import get_client_ip
from authentik.policies.reputation.models import (
CACHE_KEY_IP_PREFIX,
@ -13,6 +14,7 @@ from authentik.policies.reputation.models import (
from authentik.stages.identification.signals import identification_failed
LOGGER = get_logger()
CACHE_TIMEOUT = int(CONFIG.y("redis.cache_timeout_reputation"))
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)
# 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.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)
LOGGER.debug("Updated score", amount=amount, for_user=username, for_ip=remote_ip)

View File

@ -1,9 +1,10 @@
"""test reputation signals and policy"""
from django.contrib.auth import authenticate
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.lib.utils.http import DEFAULT_IP
from authentik.policies.reputation.models import (
CACHE_KEY_IP_PREFIX,
CACHE_KEY_USER_PREFIX,
@ -19,9 +20,12 @@ class TestReputationPolicy(TestCase):
"""test reputation signals and policy"""
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"
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)
# We need a user for the one-to-one in userreputation
self.user = User.objects.create(username=self.test_username)
@ -29,7 +33,9 @@ class TestReputationPolicy(TestCase):
def test_ip_reputation(self):
"""test IP 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
self.assertEqual(cache.get(CACHE_KEY_IP_PREFIX + self.test_ip), -1)
# Save cache and check db values
@ -39,7 +45,9 @@ class TestReputationPolicy(TestCase):
def test_user_reputation(self):
"""test User 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
self.assertEqual(cache.get(CACHE_KEY_USER_PREFIX + self.test_username), -1)
# Save cache and check db values

View File

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

View File

@ -9,7 +9,7 @@ return {}
"""
SCOPE_EMAIL_EXPRESSION = """
return {
"email": user.email,
"email": request.user.email,
"email_verified": True
}
"""
@ -17,14 +17,14 @@ SCOPE_PROFILE_EXPRESSION = """
return {
# 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.
# You can override this behaviour in custom mappings, i.e. `user.name.split(" ")`
"name": user.name,
"given_name": user.name,
# You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")`
"name": request.user.name,
"given_name": request.user.name,
"family_name": "",
"preferred_username": user.username,
"nickname": user.username,
"preferred_username": request.user.username,
"nickname": request.user.username,
# 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

@ -474,7 +474,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
now = int(time.time())
iat_time = now
exp_time = int(
now + timedelta_from_string(self.provider.token_validity).seconds
now + timedelta_from_string(self.provider.token_validity).total_seconds()
)
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
auth_events = Event.objects.filter(

View File

@ -374,9 +374,9 @@ class OAuthFulfillmentStage(StageView):
query_fragment["code"] = code.code
query_fragment["token_type"] = "bearer"
query_fragment["expires_in"] = timedelta_from_string(
self.provider.token_validity
).seconds
query_fragment["expires_in"] = int(
timedelta_from_string(self.provider.token_validity).total_seconds()
)
query_fragment["state"] = self.params.state if self.params.state else ""
return query_fragment
@ -468,14 +468,14 @@ class AuthorizationFlowInitView(PolicyAccessView):
# OpenID clients can specify a `prompt` parameter, and if its set to consent we
# need to inject a consent stage
if PROMPT_CONSNET in self.params.prompt:
if not any(isinstance(x, ConsentStageView) for x in plan.stages):
if not any(isinstance(x.stage, ConsentStageView) for x in plan.bindings):
# Plan does not have any consent stage, so we add an in-memory one
stage = ConsentStage(
name="OAuth2 Provider In-memory consent stage",
mode=ConsentMode.ALWAYS_REQUIRE,
)
plan.append(stage)
plan.append(in_memory_stage(OAuthFulfillmentStage))
plan.append_stage(stage)
plan.append_stage(in_memory_stage(OAuthFulfillmentStage))
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",

View File

@ -215,9 +215,11 @@ class TokenView(View):
"access_token": refresh_token.access_token,
"refresh_token": refresh_token.refresh_token,
"token_type": "bearer",
"expires_in": timedelta_from_string(
self.params.provider.token_validity
).seconds,
"expires_in": int(
timedelta_from_string(
self.params.provider.token_validity
).total_seconds()
),
"id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()),
}
@ -258,9 +260,11 @@ class TokenView(View):
"access_token": refresh_token.access_token,
"refresh_token": refresh_token.refresh_token,
"token_type": "bearer",
"expires_in": timedelta_from_string(
refresh_token.provider.token_validity
).seconds,
"expires_in": int(
timedelta_from_string(
refresh_token.provider.token_validity
).total_seconds()
),
"id_token": self.params.provider.encode(refresh_token.id_token.to_dict()),
}

View File

@ -1,7 +1,7 @@
"""ProxyProvider API Views"""
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.fields import CharField, ListField, SerializerMethodField
from rest_framework.serializers import ModelSerializer
@ -85,6 +85,7 @@ class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
ordering = ["name"]
@extend_schema_serializer(deprecate_fields=["forward_auth_mode"])
class ProxyOutpostConfigSerializer(ModelSerializer):
"""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.
return {
"ak_proxy": {
"user_attributes": user.group_attributes()
"user_attributes": request.user.group_attributes()
}
}"""

View File

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

View File

@ -24,6 +24,7 @@ from authentik.sources.saml.processors.constants import (
SAML_NAME_ID_FORMAT_EMAIL,
SAML_NAME_ID_FORMAT_PERSISTENT,
SAML_NAME_ID_FORMAT_TRANSIENT,
SAML_NAME_ID_FORMAT_UNSPECIFIED,
SAML_NAME_ID_FORMAT_WINDOWS,
SAML_NAME_ID_FORMAT_X509,
SIGN_ALGORITHM_TRANSFORM_MAP,
@ -165,7 +166,10 @@ class AssertionProcessor:
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_EMAIL:
name_id.text = self.http_request.user.email
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
return name_id
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_X509:
@ -180,7 +184,7 @@ class AssertionProcessor:
return name_id
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT:
# 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()
return name_id
raise UnsupportedNameIDFormat(

View File

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

View File

@ -20,10 +20,11 @@ from authentik.sources.saml.processors.constants import (
RSA_SHA256,
RSA_SHA384,
RSA_SHA512,
SAML_NAME_ID_FORMAT_EMAIL,
SAML_NAME_ID_FORMAT_UNSPECIFIED,
)
LOGGER = get_logger()
ERROR_CANNOT_DECODE_REQUEST = "Cannot decode SAML request."
ERROR_SIGNATURE_REQUIRED_BUT_ABSENT = (
"Verification Certificate configured, but request is not signed."
)
@ -42,7 +43,7 @@ class AuthNRequest:
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:
@ -69,16 +70,21 @@ class AuthNRequestParser:
auth_n_request = AuthNRequest(id=root.attrib["ID"], relay_state=relay_state)
# 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:
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
def parse(self, saml_request: str, relay_state: Optional[str]) -> AuthNRequest:
"""Validate and parse raw request with enveloped signautre."""
decoded_xml = b64decode(saml_request.encode()).decode()
try:
decoded_xml = b64decode(saml_request.encode()).decode()
except UnicodeDecodeError:
raise CannotHandleAssertion(ERROR_CANNOT_DECODE_REQUEST)
verifier = self.provider.verification_kp
@ -108,7 +114,7 @@ class AuthNRequestParser:
)
ctx.key = key
ctx.verify(signature_node)
except xmlsec.VerificationError as exc:
except xmlsec.Error as exc:
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
return self._parse_xml(decoded_xml, relay_state)
@ -121,7 +127,10 @@ class AuthNRequestParser:
sig_alg: Optional[str] = None,
) -> AuthNRequest:
"""Validate and parse raw request with detached signature"""
decoded_xml = decode_base64_and_inflate(saml_request)
try:
decoded_xml = decode_base64_and_inflate(saml_request)
except UnicodeDecodeError:
raise CannotHandleAssertion(ERROR_CANNOT_DECODE_REQUEST)
verifier = self.provider.verification_kp
@ -160,7 +169,7 @@ class AuthNRequestParser:
sign_algorithm_transform,
b64decode(signature),
)
except xmlsec.VerificationError as exc:
except xmlsec.Error as exc:
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
return self._parse_xml(decoded_xml, relay_state)

View File

@ -2,18 +2,19 @@
from base64 import b64encode
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 guardian.utils import get_anonymous_user
from authentik.crypto.models import CertificateKeyPair
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.processors.assertion import AssertionProcessor
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
from authentik.sources.saml.exceptions import MismatchedRequestID
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 (
SESSION_REQUEST_ID,
RequestProcessor,
@ -58,11 +59,6 @@ qNAZMq1DqpibfCBg
-----END CERTIFICATE-----"""
def dummy_get_response(request: HttpRequest): # pragma: no cover
"""Dummy get_response for SessionMiddleware"""
return None
class TestAuthNRequest(TestCase):
"""Test AuthN Request generator and parser"""
@ -210,5 +206,5 @@ class TestAuthNRequest(TestCase):
REDIRECT_REQUEST, REDIRECT_RELAY_STATE, REDIRECT_SIGNATURE, REDIRECT_SIG_ALG
)
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)

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.request_parser import AuthNRequest
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode, nice64
from authentik.sources.saml.exceptions import SAMLException
LOGGER = get_logger()
URL_VALIDATOR = URLValidator(schemes=("http", "https"))
@ -56,22 +57,30 @@ class SAMLFlowFinalView(ChallengeStageView):
provider: SAMLProvider = get_object_or_404(
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:
return self.executor.stage_invalid()
auth_n_request: AuthNRequest = self.request.session.pop(
SESSION_KEY_AUTH_N_REQUEST
)
response = AssertionProcessor(
provider, request, auth_n_request
).build_response()
try:
response = AssertionProcessor(
provider, request, auth_n_request
).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:
form_attrs = {

View File

@ -79,7 +79,7 @@ class SAMLSSOView(PolicyAccessView):
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
},
)
plan.append(in_memory_stage(SAMLFlowFinalView))
plan.append_stage(in_memory_stage(SAMLFlowFinalView))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",

View File

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

View File

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

View File

@ -15,6 +15,7 @@ import logging
import os
import sys
from json import dumps
from tempfile import gettempdir
from time import time
import structlog
@ -152,6 +153,7 @@ SPECTACULAR_SETTINGS = {
"url": "https://github.com/goauthentik/authentik/blob/master/LICENSE",
},
"ENUM_NAME_OVERRIDES": {
"EventActions": "authentik.events.models.EventAction",
"ChallengeChoices": "authentik.flows.challenge.ChallengeTypes",
"FlowDesignationEnum": "authentik.flows.models.FlowDesignation",
"PolicyEngineMode": "authentik.policies.models.PolicyEngineMode",
@ -193,6 +195,7 @@ CACHES = {
f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:6379"
f"/{CONFIG.y('redis.cache_db')}"
),
"TIMEOUT": int(CONFIG.y("redis.cache_timeout", 300)),
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
}
}
@ -341,7 +344,7 @@ DBBACKUP_FILENAME_TEMPLATE = "authentik-backup-{datetime}.sql"
DBBACKUP_CONNECTOR_MAPPING = {
"django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
}
DBBACKUP_TMP_DIR = gettempdir() if DEBUG else "/tmp" # nosec
if CONFIG.y("postgresql.s3_backup"):
DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
DBBACKUP_STORAGE_OPTIONS = {
@ -375,7 +378,11 @@ if _ERROR_REPORTING:
environment=CONFIG.y("error_reporting.environment", "customer"),
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(
"authentik.env", "kubernetes" if "KUBERNETES_PORT" in os.environ else "compose"
)

View File

@ -15,6 +15,10 @@ class PytestTestRunner: # pragma: no cover
settings.CELERY_TASK_ALWAYS_EAGER = True
CONFIG.y_set("authentik.avatars", "none")
CONFIG.y_set("authentik.geoip", "tests/GeoLite2-City-Test.mmdb")
CONFIG.y_set(
"outposts.docker_image_base",
"beryju.org/authentik/outpost-%(type)s:gh-master",
)
def run_tests(self, test_labels):
"""Run pytest and return the exitcode.

View File

@ -60,14 +60,21 @@ class LDAPPasswordChanger:
def check_ad_password_complexity_enabled(self) -> bool:
"""Check if DOMAIN_PASSWORD_COMPLEX is enabled"""
root_dn = self.get_domain_root_dn()
root_attrs = self._source.connection.extend.standard.paged_search(
search_base=root_dn,
search_filter="(objectClass=*)",
search_scope=ldap3.BASE,
attributes=["pwdProperties"],
)
try:
root_attrs = self._source.connection.extend.standard.paged_search(
search_base=root_dn,
search_filter="(objectClass=*)",
search_scope=ldap3.BASE,
attributes=["pwdProperties"],
)
except ldap3.core.exceptions.LDAPAttributeError:
return False
root_attrs = list(root_attrs)[0]
pwd_properties = PwdProperties(root_attrs["attributes"]["pwdProperties"])
raw_pwd_properties = root_attrs.get("attributes", {}).get("pwdProperties", None)
if raw_pwd_properties is None:
return False
pwd_properties = PwdProperties(raw_pwd_properties)
if PwdProperties.DOMAIN_PASSWORD_COMPLEX in pwd_properties:
return True

View File

@ -2,17 +2,21 @@
from authentik.lib.sentry import SentryIgnoredException
class MissingSAMLResponse(SentryIgnoredException):
class SAMLException(SentryIgnoredException):
"""Base SAML Exception"""
class MissingSAMLResponse(SAMLException):
"""Exception raised when request does not contain SAML Response."""
class UnsupportedNameIDFormat(SentryIgnoredException):
class UnsupportedNameIDFormat(SAMLException):
"""Exception raised when SAML Response contains NameID Format not supported."""
class MismatchedRequestID(SentryIgnoredException):
class MismatchedRequestID(SAMLException):
"""Exception raised when the returned request ID doesn't match the saved ID."""
class InvalidSignature(SentryIgnoredException):
class InvalidSignature(SAMLException):
"""Signature of XML Object is either missing or invalid"""

View File

@ -15,6 +15,9 @@ NS_MAP = {
SAML_NAME_ID_FORMAT_EMAIL = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
SAML_NAME_ID_FORMAT_PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
SAML_NAME_ID_FORMAT_UNSPECIFIED = (
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
)
SAML_NAME_ID_FORMAT_X509 = "urn:oasis:names:tc:SAML:2.0:nameid-format:X509SubjectName"
SAML_NAME_ID_FORMAT_WINDOWS = (
"urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName"

View File

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

View File

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

View File

@ -90,7 +90,7 @@ class InitiateView(View):
planner.allow_empty_flows = True
plan = planner.plan(self.request, kwargs)
for stage in stages_to_append:
plan.append(stage)
plan.append_stage(stage)
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",

View File

@ -9,7 +9,7 @@ from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
@ -94,7 +94,7 @@ class DuoDeviceViewSet(
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
class DuoAdminDeviceViewSet(ReadOnlyModelViewSet):
class DuoAdminDeviceViewSet(ModelViewSet):
"""Viewset for Duo authenticator devices (for admins)"""
permission_classes = [IsAdminUser]

View File

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

View File

@ -3,6 +3,7 @@ from django.http import HttpRequest, HttpResponse
from rest_framework.fields import CharField
from structlog.stdlib import get_logger
from authentik.events.models import Event, EventAction
from authentik.flows.challenge import (
Challenge,
ChallengeResponse,
@ -11,6 +12,7 @@ from authentik.flows.challenge import (
)
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views import InvalidStageError
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
LOGGER = get_logger()
@ -42,7 +44,15 @@ class AuthenticatorDuoStageView(ChallengeStageView):
def get_challenge(self, *args, **kwargs) -> Challenge:
user = self.get_pending_user()
stage: AuthenticatorDuoStage = self.executor.current_stage
enroll = stage.client.enroll(user.username)
try:
enroll = stage.client.enroll(user.username)
except RuntimeError as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message=f"Failed to enroll user: {str(exc)}",
user=user,
).from_http(self.request, user)
raise InvalidStageError(str(exc)) from exc
user_id = enroll["user_id"]
self.request.session[SESSION_KEY_DUO_USER_ID] = user_id
self.request.session[SESSION_KEY_DUO_ACTIVATION_CODE] = enroll[
@ -53,7 +63,7 @@ class AuthenticatorDuoStageView(ChallengeStageView):
"type": ChallengeTypes.NATIVE.value,
"activation_barcode": enroll["activation_barcode"],
"activation_code": enroll["activation_code"],
"stage_uuid": stage.stage_uuid,
"stage_uuid": str(stage.stage_uuid),
}
)

View File

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

View File

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

View File

@ -10,7 +10,7 @@ from authentik.flows.challenge import (
ChallengeTypes,
WithUserInfoChallenge,
)
from authentik.flows.models import NotConfiguredAction
from authentik.flows.models import NotConfiguredAction, Stage
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_validate.challenge import (
@ -143,9 +143,12 @@ class AuthenticatorValidateStageView(ChallengeStageView):
return self.executor.stage_invalid()
if stage.not_configured_action == NotConfiguredAction.CONFIGURE:
LOGGER.debug("Authenticator not configured, sending user to configure")
# Because the foreign key to stage.configuration_stage points to
# a base stage class, we need to do another lookup
stage = Stage.objects.get_subclass(pk=stage.configuration_stage.pk)
# plan.insert inserts at 1 index, so when stage_ok pops 0,
# the configuration stage is next
self.executor.plan.insert(stage.configuration_stage)
self.executor.plan.insert_stage(stage)
return self.executor.stage_ok()
return super().get(request, *args, **kwargs)

View File

@ -0,0 +1,176 @@
"""Test validator stage"""
from unittest.mock import MagicMock, patch
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase
from django.test.client import RequestFactory
from django.urls.base import reverse
from django.utils.encoding import force_str
from django_otp.plugins.otp_totp.models import TOTPDevice
from rest_framework.exceptions import ValidationError
from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.models import Flow, FlowStageBinding, NotConfiguredAction
from authentik.flows.tests.test_planner import dummy_get_response
from authentik.providers.oauth2.generators import (
generate_client_id,
generate_client_secret,
)
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_validate.api import (
AuthenticatorValidateStageSerializer,
)
from authentik.stages.authenticator_validate.challenge import (
get_challenge_for_device,
validate_challenge_code,
validate_challenge_duo,
validate_challenge_webauthn,
)
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
from authentik.stages.identification.models import IdentificationStage, UserFields
class AuthenticatorValidateStageTests(TestCase):
"""Test validator stage"""
def setUp(self) -> None:
self.user = User.objects.get(username="akadmin")
self.request_factory = RequestFactory()
def test_not_configured_action(self):
"""Test not_configured_action"""
conf_stage = IdentificationStage.objects.create(
name="conf",
user_fields=[
UserFields.USERNAME,
],
)
stage = AuthenticatorValidateStage.objects.create(
name="foo",
not_configured_action=NotConfiguredAction.CONFIGURE,
configuration_stage=conf_stage,
)
flow = Flow.objects.create(name="test", slug="test", title="test")
FlowStageBinding.objects.create(target=flow, stage=conf_stage, order=0)
FlowStageBinding.objects.create(target=flow, stage=stage, order=1)
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{"uid_field": "akadmin"},
)
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-identification",
"password_fields": False,
"primary_action": "Log in",
"flow_info": {
"background": flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
"title": flow.title,
},
"user_fields": ["username"],
"sources": [],
},
)
def test_stage_validation(self):
"""Test serializer validation"""
self.client.force_login(self.user)
serializer = AuthenticatorValidateStageSerializer(
data={"name": "foo", "not_configured_action": NotConfiguredAction.CONFIGURE}
)
self.assertFalse(serializer.is_valid())
self.assertIn("not_configured_action", serializer.errors)
def test_device_challenge_totp(self):
"""Test device challenge"""
request = self.request_factory.get("/")
totp_device = TOTPDevice.objects.create(
user=self.user, confirmed=True, digits=6
)
self.assertEqual(get_challenge_for_device(request, totp_device), {})
with self.assertRaises(ValidationError):
validate_challenge_code("1234", request, self.user)
def test_device_challenge_webauthn(self):
"""Test webauthn"""
request = self.request_factory.get("/")
request.user = self.user
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
webauthn_device = WebAuthnDevice.objects.create(
user=self.user,
public_key="qwerqwerqre",
credential_id="foobarbaz",
sign_count=0,
rp_id="foo",
)
challenge = get_challenge_for_device(request, webauthn_device)
del challenge["challenge"]
self.assertEqual(
challenge,
{
"allowCredentials": [
{
"id": "foobarbaz",
"transports": ["usb", "nfc", "ble", "internal"],
"type": "public-key",
}
],
"rpId": "foo",
"timeout": 60000,
"userVerification": "discouraged",
},
)
with self.assertRaises(ValidationError):
validate_challenge_webauthn({}, request, self.user)
def test_device_challenge_duo(self):
"""Test duo"""
request = self.request_factory.get("/")
stage = AuthenticatorDuoStage.objects.create(
name="test",
client_id=generate_client_id(),
client_secret=generate_client_secret(),
api_hostname="",
)
duo_device = DuoDevice.objects.create(
user=self.user,
stage=stage,
)
duo_mock = MagicMock(
auth=MagicMock(
return_value={
"result": "allow",
"status": "allow",
"status_msg": "Success. Logging you in...",
}
)
)
failed_duo_mock = MagicMock(auth=MagicMock(return_value={"result": "deny"}))
with patch(
"authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.client",
duo_mock,
):
self.assertEqual(
duo_device.pk, validate_challenge_duo(duo_device.pk, request, self.user)
)
with patch(
"authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.client",
failed_duo_mock,
):
with self.assertRaises(ValidationError):
validate_challenge_duo(duo_device.pk, request, self.user)

View File

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

View File

@ -36,12 +36,14 @@ class TestCaptchaStage(TestCase):
public_key=RECAPTCHA_PUBLIC_KEY,
private_key=RECAPTCHA_PRIVATE_KEY,
)
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
def test_valid(self):
"""Test valid captcha"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan

View File

@ -39,9 +39,11 @@ class TestConsentStage(TestCase):
stage = ConsentStage.objects.create(
name="consent", mode=ConsentMode.ALWAYS_REQUIRE
)
FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
plan = FlowPlan(flow_pk=flow.pk.hex, stages=[stage], markers=[StageMarker()])
plan = FlowPlan(
flow_pk=flow.pk.hex, bindings=[binding], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
@ -69,11 +71,11 @@ class TestConsentStage(TestCase):
designation=FlowDesignation.AUTHENTICATION,
)
stage = ConsentStage.objects.create(name="consent", mode=ConsentMode.PERMANENT)
FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
plan = FlowPlan(
flow_pk=flow.pk.hex,
stages=[stage],
bindings=[binding],
markers=[StageMarker()],
context={PLAN_CONTEXT_APPLICATION: self.application},
)
@ -110,11 +112,11 @@ class TestConsentStage(TestCase):
stage = ConsentStage.objects.create(
name="consent", mode=ConsentMode.EXPIRING, consent_expire_in="seconds=1"
)
FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
plan = FlowPlan(
flow_pk=flow.pk.hex,
stages=[stage],
bindings=[binding],
markers=[StageMarker()],
context={PLAN_CONTEXT_APPLICATION: self.application},
)

View File

@ -26,12 +26,14 @@ class TestUserDenyStage(TestCase):
designation=FlowDesignation.AUTHENTICATION,
)
self.stage = DenyStage.objects.create(name="logout")
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
def test_valid_password(self):
"""Test with a valid pending user and backend"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan

View File

@ -34,12 +34,14 @@ class TestEmailStageSending(TestCase):
self.stage = EmailStage.objects.create(
name="email",
)
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
def test_pending_user(self):
"""Test with pending user"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
@ -67,7 +69,7 @@ class TestEmailStageSending(TestCase):
def test_send_error(self):
"""Test error during sending (sending will be retried)"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session

View File

@ -35,12 +35,14 @@ class TestEmailStage(TestCase):
self.stage = EmailStage.objects.create(
name="email",
)
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
def test_rendering(self):
"""Test with pending user"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
@ -56,7 +58,7 @@ class TestEmailStage(TestCase):
def test_without_user(self):
"""Test without pending user"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
@ -71,7 +73,7 @@ class TestEmailStage(TestCase):
def test_pending_user(self):
"""Test with pending user"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
@ -102,7 +104,7 @@ class TestEmailStage(TestCase):
# Make sure token exists
self.test_pending_user()
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan

View File

@ -0,0 +1,31 @@
# Generated by Django 3.2.4 on 2021-06-14 15:32
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_identification", "0010_identificationstage_password_stage"),
]
operations = [
migrations.AlterField(
model_name="identificationstage",
name="user_fields",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(
choices=[
("email", "E Mail"),
("username", "Username"),
("upn", "Upn"),
],
max_length=100,
),
blank=True,
help_text="Fields of the user object to match against. (Hold shift to select multiple options)",
size=None,
),
),
]

View File

@ -17,6 +17,7 @@ class UserFields(models.TextChoices):
E_MAIL = "email"
USERNAME = "username"
UPN = "upn"
class IdentificationStage(Stage):

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