Compare commits

...

102 Commits

Author SHA1 Message Date
adc4cd9c0d release: 2021.6.4 2021-07-05 16:59:29 +02:00
abed254ca1 web/admin: make table dispatch refresh event on refresh button instead of just fetching
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-05 09:48:14 +02:00
edfab0995f build(deps): bump eslint from 7.29.0 to 7.30.0 in /web (#1106) 2021-07-05 09:10:15 +02:00
528dedf99d build(deps): bump chart.js from 3.4.0 to 3.4.1 in /web (#1107) 2021-07-05 09:09:33 +02:00
5d7eec3049 build(deps): bump @types/chart.js from 2.9.32 to 2.9.33 in /web (#1108) 2021-07-05 09:09:24 +02:00
ad44567ebe build(deps): bump packaging from 20.9 to 21.0 (#1109) 2021-07-05 09:09:13 +02:00
ac82002339 build(deps): bump boto3 from 1.17.104 to 1.17.105 (#1110) 2021-07-05 09:08:53 +02:00
df92111296 outposts: update outpost permissions on m2m change
closes #1105

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-04 19:37:12 +02:00
da8417a141 outposts/ldap: re-add old fields for backwards compatibility
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-04 18:10:39 +02:00
7f32355e3e website/docs: update release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-04 13:49:38 +02:00
5afe88a605 outposts: fix empty message when docker outpost controller has changed nothing
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-04 13:48:43 +02:00
320dab3425 core: only show Reset password link when recovery flow is configured
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-04 12:59:41 +02:00
ca44f8bd60 web: log response when >= http 400
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-04 12:39:10 +02:00
5fd408ca82 outposts: fix docker controller not checking ports correctly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-04 12:32:55 +02:00
becb9e34b5 outposts: fix docker controller not checking env correctly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 22:17:29 +02:00
4917ab9985 outposts: fix container not being started after creation
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 21:59:47 +02:00
bd92505bc2 core: add notice about duplicate keys
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 21:52:28 +02:00
30033d1f90 g: fix static and media caching not working properly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 21:43:37 +02:00
3e5dfcbd0f website/docs: add release notes for 2021.6.4
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 21:29:52 +02:00
bf0141acc6 crypto: fix linting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 19:57:25 +02:00
0c8d513567 stages/user_write: add wrapper for post to user_write
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 19:25:37 +02:00
d07704fdf1 crypto: show both sha1 and sha256 fingerprints
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 19:25:27 +02:00
086a8753c0 flows: handle old cached flow plans better
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 19:22:09 +02:00
ae7a6e2fd6 website/docs: fix gitab saml binding
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 19:02:47 +02:00
6a4ddcaba7 web/admin: don't use form.reset() for ModelForms, reset instance
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 18:26:50 +02:00
2c9b596f01 web/admin: run explicit update after loading instance
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 16:41:42 +02:00
7257108091 sources/oauth: create configuration error event when profile can't be parsed as json
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 16:11:49 +02:00
91f7b289cc web/admin: show oauth2 token revoked status
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 16:04:24 +02:00
77a507d2f8 providers/oauth2: add revoked field, create suspicious event when previous token is used
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 15:59:01 +02:00
3e60e956f4 providers/oauth2: fix CORS headers not being set for unsuccessful requests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 15:49:00 +02:00
84ec70c2a2 providers/oauth2: use self.expires for exp field instead of calculating it again
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 15:32:58 +02:00
72846f0ae1 website/docs: update system requirements
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-03 15:11:40 +02:00
dd53e7e9b1 web/admin: fix ModelForm not re-loading after being reset
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-02 21:21:11 +02:00
9df16a9ae0 website/docs: update gitlab docs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-02 21:17:16 +02:00
02dd44eeec build(deps): bump rollup from 2.52.4 to 2.52.7 in /web (#1100) 2021-07-02 08:04:31 +02:00
2f78e14381 build(deps): bump channels-redis from 3.2.0 to 3.3.0 (#1101) 2021-07-02 08:04:09 +02:00
ef6f692526 build(deps): bump boto3 from 1.17.102 to 1.17.104 (#1102) 2021-07-02 08:03:58 +02:00
2dd575874b build(deps): bump django from 3.2.4 to 3.2.5 (#1103) 2021-07-02 08:03:48 +02:00
84c2ebabaa build(deps-dev): bump pylint from 2.9.1 to 2.9.3 (#1104) 2021-07-02 08:03:34 +02:00
3e26170f4b providers/oauth2: deepmerge claims
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-01 17:33:46 +02:00
4709dca33c outposts/proxy: always redirect to session-end interface on sign_out
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-01 16:51:36 +02:00
6064a481fb outposts/proxy: set ValidateURL
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-01 15:42:48 +02:00
3979b0bde7 tests/e2e: ensure superuser group is created
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-01 12:16:58 +02:00
4280847bcc tests/e2e: add LDAP bind and search tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-01 11:51:07 +02:00
ade8644da6 outposts/ldap: add support for boolean fields in ldap
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-01 11:51:07 +02:00
3c3fd53999 build(deps): bump typescript from 4.3.4 to 4.3.5 in /web (#1097)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.3.4 to 4.3.5.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.3.4...v4.3.5)

---
updated-dependencies:
- dependency-name: typescript
  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-07-01 09:42:20 +02:00
7b823f23ae build(deps): bump actions/setup-node from 2.1.5 to 2.2.0 (#1098)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.1.5 to 2.2.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v2.1.5...v2.2.0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  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-07-01 09:42:08 +02:00
a67bea95d4 build(deps-dev): bump pylint from 2.9.0 to 2.9.1 (#1099)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.9.0 to 2.9.1.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.9.0...v2.9.1)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-01 09:41:42 +02:00
775e0ef2fa website/docs: improve docs for restore in k8s
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-30 19:07:11 +02:00
d102c59654 build(deps-dev): bump pylint from 2.8.3 to 2.9.0 (#1095)
* build(deps-dev): bump pylint from 2.8.3 to 2.9.0

Bumps [pylint](https://github.com/PyCQA/pylint) from 2.8.3 to 2.9.0.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.8.3...v2.9.0)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* *: update source for new pylint version

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-30 10:37:28 +02:00
03448a9169 build(deps): bump rollup from 2.52.3 to 2.52.4 in /web (#1094)
Bumps [rollup](https://github.com/rollup/rollup) from 2.52.3 to 2.52.4.
- [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.52.3...v2.52.4)

---
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-30 09:38:53 +02:00
1e6c081e5c website/docs: update forward_auth for nginx config
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-29 20:32:49 +02:00
8b9ce4a745 ci: don't finalise sentry release
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-29 17:08:57 +02:00
014d93d485 root: fix mismatched version in openapi schema
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-29 16:34:42 +02:00
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
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
141 changed files with 2281 additions and 831 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2021.6.2
current_version = 2021.6.4
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
@ -21,6 +21,8 @@ values =
[bumpversion:file:docker-compose.yml]
[bumpversion:file:schema.yml]
[bumpversion:file:.github/workflows/release.yml]
[bumpversion:file:authentik/__init__.py]

View File

@ -33,14 +33,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik:2021.6.2,
beryju/authentik:2021.6.4,
beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.6.2,
ghcr.io/goauthentik/server:2021.6.4,
ghcr.io/goauthentik/server:latest
platforms: linux/amd64,linux/arm64
context: .
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.6.2', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }}
run: |
docker pull beryju/authentik:latest
docker tag beryju/authentik:latest beryju/authentik:stable
@ -75,14 +75,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-proxy:2021.6.2,
beryju/authentik-proxy:2021.6.4,
beryju/authentik-proxy:latest,
ghcr.io/goauthentik/proxy:2021.6.2,
ghcr.io/goauthentik/proxy:2021.6.4,
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.2', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }}
run: |
docker pull beryju/authentik-proxy:latest
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
@ -117,14 +117,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-ldap:2021.6.2,
beryju/authentik-ldap:2021.6.4,
beryju/authentik-ldap:latest,
ghcr.io/goauthentik/ldap:2021.6.2,
ghcr.io/goauthentik/ldap:2021.6.4,
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.2', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }}
run: |
docker pull beryju/authentik-ldap:latest
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
@ -157,7 +157,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.5
uses: actions/setup-node@v2.2.0
with:
node-version: 12.x
- name: Build web api client and web ui
@ -176,6 +176,7 @@ jobs:
SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org
with:
version: authentik@2021.6.2
version: authentik@2021.6.4
environment: beryjuorg-prod
sourcemaps: './web/dist'
finalize: false

184
Pipfile.lock generated
View File

@ -76,11 +76,11 @@
},
"asgiref": {
"hashes": [
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
"sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9",
"sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"
],
"markers": "python_version >= '3.6'",
"version": "==3.3.4"
"version": "==3.4.1"
},
"async-timeout": {
"hashes": [
@ -122,19 +122,19 @@
},
"boto3": {
"hashes": [
"sha256:2c2f70608934b03f9c08f4cd185de223b5abd18245dd4d4800e1fbc2a2523e31",
"sha256:fccfa81cda69bb2317ed97e7149d7d84d19e6ec3bfbe3f721139e7ac0c407c73"
"sha256:3b35689c215c982fe9f7ef78d748aa9b0cd15c3b2eb04f9b460aaa63fe2fbd03",
"sha256:b1cbeb92123799001b97f2ee1cdf470e21f1be08314ae28fc7ea357925186f1c"
],
"index": "pypi",
"version": "==1.17.98"
"version": "==1.17.105"
},
"botocore": {
"hashes": [
"sha256:b2a49de4ee04b690142c8e7240f0f5758e3f7673dd39cf398efe893bf5e11c3f",
"sha256:b955b23fe2fbdbbc8e66f37fe2970de6b5d8169f940b200bcf434751709d38f6"
"sha256:b0fda4edf8eb105453890700d49011ada576d0cc7326a0699dfabe9e872f552c",
"sha256:b5ba72d22212b0355f339c2a98b3296b3b2202a48e6a2b1366e866bc65a64b67"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.20.98"
"version": "==1.20.105"
},
"cachetools": {
"hashes": [
@ -165,11 +165,11 @@
},
"celery": {
"hashes": [
"sha256:54436cd97b031bf2e08064223240e2a83d601d9414bcb1b702f94c6c33c29485",
"sha256:b5399d76cf70d5cfac3ec993f8796ec1aa90d4cef55972295751f384758a80d7"
"sha256:8d9a3de9162965e97f8e8cc584c67aad83b3f7a267584fa47701ed11c3e0d4b0",
"sha256:9dab2170b4038f7bf10ef2861dbf486ddf1d20592290a1040f7b7a1259705d42"
],
"index": "pypi",
"version": "==5.1.1"
"version": "==5.1.2"
},
"certifi": {
"hashes": [
@ -242,11 +242,11 @@
},
"channels-redis": {
"hashes": [
"sha256:18d63f6462a58011740dc8eeb57ea4b31ec220eb551cb71b27de9c6779a549de",
"sha256:2fb31a63b05373f6402da2e6a91a22b9e66eb8b56626c6bfc93e156c734c5ae6"
"sha256:0a18ce279c15ba79b7985bb12b2d6dd0ac8a14e4ad6952681f4422a4cc4a5ea9",
"sha256:1abd5820ff1ed4ac627f8a219ad389e4c87e52e47a230929a7a474e95dd2c6c2"
],
"index": "pypi",
"version": "==3.2.0"
"version": "==3.3.0"
},
"chardet": {
"hashes": [
@ -342,11 +342,11 @@
},
"django": {
"hashes": [
"sha256:66c9d8db8cc6fe938a28b7887c1596e42d522e27618562517cc8929eb7e7f296",
"sha256:ea735cbbbb3b2fba6d4da4784a0043d84c67c92f1fdf15ad6db69900e792c10f"
"sha256:3da05fea54fdec2315b54a563d5b59f3b4e2b1e69c3a5841dda35019c01855cd",
"sha256:c58b5f19c5ae0afe6d75cbdd7df561e6eb929339985dbbda2565e1cabb19a62e"
],
"index": "pypi",
"version": "==3.2.4"
"version": "==3.2.5"
},
"django-dbbackup": {
"git": "https://github.com/django-dbbackup/django-dbbackup.git",
@ -473,11 +473,11 @@
},
"google-auth": {
"hashes": [
"sha256:b3a67fa9ba5b768861dacf374c2135eb09fa14a0e40c851c3b8ea7abe6fc8fef",
"sha256:e34e5f5de5610b202f9b40ebd9f8b27571d5c5537db9afed3a72b2db5a345039"
"sha256:9266252e11393943410354cf14a77bcca24dd2ccd9c4e1aef23034fe0fbae630",
"sha256:c7c215c74348ef24faef2f7b62f6d8e6b38824fe08b1e7b7b09a02d397eda7b3"
],
"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"
"version": "==1.32.1"
},
"gunicorn": {
"hashes": [
@ -778,11 +778,11 @@
},
"packaging": {
"hashes": [
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
],
"index": "pypi",
"version": "==20.9"
"version": "==21.0"
},
"prometheus-client": {
"hashes": [
@ -948,10 +948,30 @@
},
"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"
],
"markers": "python_version >= '3.5'",
"version": "==0.17.3"
"markers": "python_version >= '3.6'",
"version": "==0.18.0"
},
"python-dateutil": {
"hashes": [
@ -1167,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": [
@ -1403,11 +1423,11 @@
},
"astroid": {
"hashes": [
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
"sha256:38b95085e9d92e2ca06cf8b35c12a74fa81da395a6f9e65803742e6509c05892",
"sha256:606b2911d10c3dcf35e58d2ee5c97360e8477d7b9f3efc3f24811c93e6fc2cd9"
],
"markers": "python_version ~= '3.6'",
"version": "==2.5.6"
"version": "==2.6.2"
},
"attrs": {
"hashes": [
@ -1612,11 +1632,11 @@
},
"packaging": {
"hashes": [
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
],
"index": "pypi",
"version": "==20.9"
"version": "==21.0"
},
"pathspec": {
"hashes": [
@ -1651,11 +1671,11 @@
},
"pylint": {
"hashes": [
"sha256:0a049c5d47b629d9070c3932d13bff482b12119b6a241a93bc460b0be16953c8",
"sha256:792b38ff30903884e4a9eab814ee3523731abd3c463f3ba48d7b627e87013484"
"sha256:23a1dc8b30459d78e9ff25942c61bb936108ccbe29dd9e71c01dc8274961709a",
"sha256:5d46330e6b8886c31b5e3aba5ff48c10f4aa5e76cbf9002c6544306221e63fbc"
],
"index": "pypi",
"version": "==2.8.3"
"version": "==2.9.3"
},
"pylint-django": {
"hashes": [
@ -1733,49 +1753,45 @@
},
"regex": {
"hashes": [
"sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5",
"sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79",
"sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31",
"sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500",
"sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11",
"sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14",
"sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3",
"sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439",
"sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c",
"sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82",
"sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711",
"sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093",
"sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a",
"sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb",
"sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8",
"sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17",
"sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000",
"sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d",
"sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480",
"sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc",
"sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0",
"sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9",
"sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765",
"sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e",
"sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a",
"sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07",
"sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f",
"sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac",
"sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7",
"sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed",
"sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968",
"sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7",
"sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2",
"sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4",
"sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87",
"sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8",
"sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10",
"sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29",
"sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605",
"sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6",
"sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"
"sha256:0e46c1191b2eb293a6912269ed08b4512e7e241bbf591f97e527492e04c77e93",
"sha256:18040755606b0c21281493ec309214bd61e41a170509e5014f41d6a5a586e161",
"sha256:1806370b2bef4d4193eebe8ee59a9fd7547836a34917b7badbe6561a8594d9cb",
"sha256:1ccbd41dbee3a31e18938096510b7d4ee53aa9fce2ee3dcc8ec82ae264f6acfd",
"sha256:1d386402ae7f3c9b107ae5863f7ecccb0167762c82a687ae6526b040feaa5ac6",
"sha256:210c359e6ee5b83f7d8c529ba3c75ba405481d50f35a420609b0db827e2e3bb5",
"sha256:268fe9dd1deb4a30c8593cabd63f7a241dfdc5bd9dd0233906c718db22cdd49a",
"sha256:361be4d311ac995a8c7ad577025a3ae3a538531b1f2cf32efd8b7e5d33a13e5a",
"sha256:3f7a92e60930f8fca2623d9e326c173b7cf2c8b7e4fdcf984b75a1d2fb08114d",
"sha256:444723ebaeb7fa8125f29c01a31101a3854ac3de293e317944022ae5effa53a4",
"sha256:494d0172774dc0beeea984b94c95389143db029575f7ca908edd74469321ea99",
"sha256:4b1999ef60c45357598935c12508abf56edbbb9c380df6f336de38a6c3a294ae",
"sha256:4fc86b729ab88fe8ac3ec92287df253c64aa71560d76da5acd8a2e245839c629",
"sha256:5049d00dbb78f9d166d1c704e93934d42cce0570842bb1a61695123d6b01de09",
"sha256:56bef6b414949e2c9acf96cb5d78de8b529c7b99752619494e78dc76f99fd005",
"sha256:59845101de68fd5d3a1145df9ea022e85ecd1b49300ea68307ad4302320f6f61",
"sha256:6b8b629f93246e507287ee07e26744beaffb4c56ed520576deac8b615bd76012",
"sha256:6c72ebb72e64e9bd195cb35a9b9bbfb955fd953b295255b8ae3e4ad4a146b615",
"sha256:7743798dfb573d006f1143d745bf17efad39775a5190b347da5d83079646be56",
"sha256:78a2a885345a2d60b5e68099e877757d5ed12e46ba1e87507175f14f80892af3",
"sha256:849802379a660206277675aa5a5c327f5c910c690649535863ddf329b0ba8c87",
"sha256:8cf6728f89b071bd3ab37cb8a0e306f4de897553a0ed07442015ee65fbf53d62",
"sha256:a1b6a3f600d6aff97e3f28c34192c9ed93fee293bd96ef327b64adb51a74b2f6",
"sha256:a548bb51c4476332ce4139df8e637386730f79a92652a907d12c696b6252b64d",
"sha256:a8a5826d8a1b64e2ff9af488cc179e1a4d0f144d11ce486a9f34ea38ccedf4ef",
"sha256:b024ee43ee6b310fad5acaee23e6485b21468718cb792a9d1693eecacc3f0b7e",
"sha256:b092754c06852e8a8b022004aff56c24b06310189186805800d09313c37ce1f8",
"sha256:b1dbeef938281f240347d50f28ae53c4b046a23389cd1fc4acec5ea0eae646a1",
"sha256:bf819c5b77ff44accc9a24e31f1f7ceaaf6c960816913ed3ef8443b9d20d81b6",
"sha256:c11f2fca544b5e30a0e813023196a63b1cb9869106ef9a26e9dae28bce3e4e26",
"sha256:ce269e903b00d1ab4746793e9c50a57eec5d5388681abef074d7b9a65748fca5",
"sha256:d0cf2651a8804f6325747c7e55e3be0f90ee2848e25d6b817aa2728d263f9abb",
"sha256:e07e92935040c67f49571779d115ecb3e727016d42fb36ee0d8757db4ca12ee0",
"sha256:e80d2851109e56420b71f9702ad1646e2f0364528adbf6af85527bc61e49f394",
"sha256:ed77b97896312bc2deafe137ca2626e8b63808f5bedb944f73665c68093688a7",
"sha256:f32f47fb22c988c0b35756024b61d156e5c4011cb8004aa53d93b03323c45657",
"sha256:fdad3122b69cdabdb3da4c2a4107875913ac78dab0117fc73f988ad589c66b66"
],
"version": "==2021.4.4"
"version": "==2021.7.1"
},
"requests": {
"hashes": [
@ -1838,11 +1854,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

@ -1,3 +1,3 @@
"""authentik"""
__version__ = "2021.6.2"
__version__ = "2021.6.4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -19,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")

View File

@ -2,12 +2,11 @@
from json import loads
from django.db.models.query import QuerySet
from django.http.response import Http404
from django.urls import reverse_lazy
from django.utils.http import urlencode
from django_filters.filters import BooleanFilter, CharFilter
from django_filters.filterset import FilterSet
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_field
from drf_spectacular.utils import extend_schema, extend_schema_field
from guardian.utils import get_anonymous_user
from rest_framework.decorators import action
from rest_framework.fields import CharField, JSONField, SerializerMethodField
@ -173,7 +172,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
@extend_schema(
responses={
"200": LinkSerializer(many=False),
"404": OpenApiResponse(description="No recovery flow found."),
"404": LinkSerializer(many=False),
},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@ -184,7 +183,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
# Check that there is a recovery flow, if not return an error
flow = tenant.flow_recovery
if not flow:
raise Http404
return Response({"link": ""}, status=404)
user: User = self.get_object()
token, __ = Token.objects.get_or_create(
identifier=f"{user.uid}-password-reset",

View File

@ -14,7 +14,9 @@ def is_dict(value: Any):
"""Ensure a value is a dictionary, useful for JSONFields"""
if isinstance(value, dict):
return
raise ValidationError("Value must be a dictionary.")
raise ValidationError(
"Value must be a dictionary, and not have any duplicate keys."
)
class PassiveSerializer(Serializer):

View File

@ -5,14 +5,13 @@ from typing import Any, Optional, Type
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
from django.core import validators
from django.db import models
from django.db.models import Q, QuerySet
from django.db.models import Q, QuerySet, options
from django.http import HttpRequest
from django.templatetags.static import static
from django.utils.functional import cached_property

View File

@ -213,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

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

View File

@ -10,7 +10,7 @@
{% block head %}
<style>
.pf-c-background-image::before {
background-image: url("/static/dist/assets/images/flow_background.jpg");
--ak-flow-background: url("/static/dist/assets/images/flow_background.jpg");
}
</style>
{% endblock %}

View File

@ -97,7 +97,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
fields = [
"pk",
"name",
"fingerprint",
"fingerprint_sha256",
"fingerprint_sha1",
"certificate_data",
"key_data",
"cert_expiry",

View File

@ -16,11 +16,6 @@ from authentik.crypto.models import CertificateKeyPair
class CertificateBuilder:
"""Build self-signed certificates"""
__public_key = None
__private_key = None
__builder = None
__certificate = None
common_name: str
def __init__(self):

View File

@ -68,12 +68,19 @@ class CertificateKeyPair(CreatedUpdatedModel):
return self._private_key
@property
def fingerprint(self) -> str:
def fingerprint_sha256(self) -> str:
"""Get SHA256 Fingerprint of certificate_data"""
return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode(
"utf-8"
)
@property
def fingerprint_sha1(self) -> str:
"""Get SHA1 Fingerprint of certificate_data"""
return hexlify(
self.certificate.fingerprint(hashes.SHA1()), ":" # nosec
).decode("utf-8")
@property
def kid(self):
"""Get Key ID used for JWKS"""

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
@ -96,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

@ -46,7 +46,7 @@ class NotificationTransportTestSerializer(Serializer):
messages = ListField(child=CharField())
def create(self, request: Request) -> Response:
def create(self, validated_data: Request) -> Response:
raise NotImplementedError
def update(self, request: Request) -> Response:

View File

@ -27,10 +27,9 @@ class GeoIPDict(TypedDict):
class GeoIPReader:
"""Slim wrapper around GeoIP API"""
__reader: Optional[Reader] = None
__last_mtime: float = 0.0
def __init__(self):
self.__reader: Optional[Reader] = None
self.__last_mtime: float = 0.0
self.__open()
def __open(self):

View File

@ -3,6 +3,7 @@ from functools import partial
from typing import Callable
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from django.db.models import Model
from django.db.models.signals import post_save, pre_delete
from django.http import HttpRequest, HttpResponse
@ -13,6 +14,7 @@ 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
@ -62,12 +64,21 @@ class AuditMiddleware:
if settings.DEBUG:
return
thread = EventNewThread(
EventAction.SYSTEM_EXCEPTION,
request,
message=exception_to_string(exception),
)
thread.run()
# Special case for SuspiciousOperation, we have a special event action for that
if isinstance(exception, SuspiciousOperation):
thread = EventNewThread(
EventAction.SUSPICIOUS_REQUEST,
request,
message=str(exception),
)
thread.run()
elif 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

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

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

@ -135,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

@ -0,0 +1,26 @@
# Generated by Django 3.2.4 on 2021-07-03 13:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0021_flowstagebinding_invalid_response_action"),
]
operations = [
migrations.AlterField(
model_name="flowstagebinding",
name="invalid_response_action",
field=models.TextField(
choices=[
("retry", "Retry"),
("restart", "Restart"),
("restart_with_context", "Restart With Context"),
],
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. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT restarts the flow while keeping the current context.",
),
),
]

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

@ -52,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
@ -89,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:
@ -161,7 +169,7 @@ class FlowPlanner:
plan = self._build_plan(user, request, default_context)
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
@ -216,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,6 +16,7 @@ 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
@ -69,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
@ -77,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)
@ -126,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

@ -40,15 +40,11 @@ def transaction_rollback():
class FlowImporter:
"""Import Flow from json"""
__import: FlowBundle
__pk_map: dict[Any, Model]
logger: BoundLogger
def __init__(self, json_input: str):
self.__pk_map: dict[Any, Model] = {}
self.logger = get_logger()
self.__pk_map = {}
import_dict = loads(json_input)
try:
self.__import = from_dict(FlowBundle, import_dict)

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,7 +38,13 @@ 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,
@ -107,6 +114,7 @@ class FlowExecutorView(APIView):
flow: Flow
plan: Optional[FlowPlan] = None
current_binding: FlowStageBinding
current_stage: Stage
current_stage_view: View
@ -126,7 +134,7 @@ class FlowExecutorView(APIView):
message = exc.__doc__ if exc.__doc__ else str(exc)
return self.stage_invalid(error_message=message)
# pylint: disable=unused-argument
# pylint: disable=unused-argument, too-many-return-statements
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
# Early check if theres an active Plan for the current session
if SESSION_KEY_PLAN in self.request.session:
@ -159,11 +167,23 @@ 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:
try:
# This is the first time we actually access any attribute on the selected plan
# if the cached plan is from an older version, it might have different attributes
# in which case we just delete the plan and invalidate everything
next_binding = self.plan.next(self.request)
except Exception as exc: # pylint: disable=broad-except
self._logger.warning(
"f(exec): found incompatible flow plan, invalidating run", exc=exc
)
keys = cache.keys("flow_*")
cache.delete_many(keys)
return self.stage_invalid()
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,
@ -268,8 +288,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
@ -293,10 +336,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})

View File

@ -26,10 +26,9 @@ class ConfigLoader:
loaded_file = []
__config = {}
def __init__(self):
super().__init__()
self.__config = {}
base_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "../.."))
for path in SEARCH_PATHS:
# Check if path is relative, and if so join with base_dir

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

@ -51,7 +51,7 @@ class OutpostSerializer(ModelSerializer):
raise ValidationError(
(
f"Outpost type {self.initial_data['type']} can't be used with "
f"{type(provider)} providers."
f"{provider.__class__.__name__} providers."
)
)
return providers

View File

@ -69,7 +69,7 @@ class OutpostConsumer(AuthJsonConsumer):
self.last_uid = self.channel_name
# pylint: disable=unused-argument
def disconnect(self, close_code):
def disconnect(self, code):
if self.outpost and self.last_uid:
state = OutpostState.for_instance_uid(self.outpost, self.last_uid)
if self.channel_name in state.channel_ids:

View File

@ -36,8 +36,10 @@ class DockerController(BaseController):
def _get_env(self) -> dict[str, str]:
return {
"AUTHENTIK_HOST": self.outpost.config.authentik_host,
"AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure),
"AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(),
"AUTHENTIK_INSECURE": str(
self.outpost.config.authentik_host_insecure
).lower(),
"AUTHENTIK_TOKEN": self.outpost.token.key,
}
@ -45,11 +47,34 @@ class DockerController(BaseController):
"""Check if container's env is equal to what we would set. Return true if container needs
to be rebuilt."""
should_be = self._get_env()
container_env = container.attrs.get("Config", {}).get("Env", {})
container_env = container.attrs.get("Config", {}).get("Env", [])
for key, expected_value in should_be.items():
if key not in container_env:
continue
if container_env[key] != expected_value:
entry = f"{key.upper()}={expected_value}"
if entry not in container_env:
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
# {'3389/tcp': [
# {'HostIp': '0.0.0.0', 'HostPort': '389'},
# {'HostIp': '::', 'HostPort': '389'}
# ]}
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") == str(port.port)
if not host_matching:
return True
return False
@ -58,7 +83,7 @@ class DockerController(BaseController):
try:
return self.client.containers.get(container_name), False
except NotFound:
self.logger.info("Container does not exist, creating")
self.logger.info("(Re-)creating container...")
image_name = self.get_container_image()
self.client.images.pull(image_name)
container_args = {
@ -86,6 +111,7 @@ class DockerController(BaseController):
try:
container, has_been_created = self._get_container()
if has_been_created:
container.start()
return None
# Check if the container is out of date, delete it and retry
if len(container.image.tags) > 0:
@ -98,6 +124,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...")
@ -138,6 +169,7 @@ class DockerController(BaseController):
self.logger.info("Container is not running, restarting...")
container.start()
return None
self.logger.info("Container is running")
return None
except DockerException as exc:
raise ControllerException(str(exc)) from exc

View File

@ -405,7 +405,10 @@ class Outpost(models.Model):
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

@ -9,7 +9,7 @@ CELERY_BEAT_SCHEDULE = {
},
"outposts_service_connection_check": {
"task": "authentik.outposts.tasks.outpost_service_connection_monitor",
"schedule": crontab(minute="*/60"),
"schedule": crontab(minute="*/5"),
"options": {"queue": "authentik_scheduled"},
},
"outpost_token_ensurer": {

View File

@ -1,7 +1,7 @@
"""authentik outpost signals"""
from django.core.cache import cache
from django.db.models import Model
from django.db.models.signals import post_save, pre_delete, pre_save
from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
from django.dispatch import receiver
from structlog.stdlib import get_logger
@ -46,6 +46,14 @@ def pre_save_outpost(sender, instance: Outpost, **_):
outpost_controller.delay(instance.pk.hex, action="down", from_cache=True)
@receiver(m2m_changed, sender=Outpost.providers.through)
# pylint: disable=unused-argument
def m2m_changed_update(sender, instance: Model, action: str, **_):
"""Update outpost on m2m change, when providers are added or removed"""
if action in ["post_add", "post_remove", "post_clear"]:
outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)
@receiver(post_save)
# pylint: disable=unused-argument
def post_save_update(sender, instance: Model, **_):

View File

@ -82,13 +82,13 @@ class PolicyBindingSerializer(ModelSerializer):
"timeout",
]
def validate(self, data: OrderedDict) -> OrderedDict:
def validate(self, attrs: OrderedDict) -> OrderedDict:
"""Check that either policy, group or user is set."""
count = sum(
[
bool(data.get("policy", None)),
bool(data.get("group", None)),
bool(data.get("user", None)),
bool(attrs.get("policy", None)),
bool(attrs.get("group", None)),
bool(attrs.get("user", None)),
]
)
invalid = count > 1
@ -97,7 +97,7 @@ class PolicyBindingSerializer(ModelSerializer):
raise ValidationError("Only one of 'policy', 'group' or 'user' can be set.")
if empty:
raise ValidationError("One of 'policy', 'group' or 'user' must be set.")
return data
return attrs
class PolicyBindingViewSet(UsedByMixin, ModelViewSet):

View File

@ -62,12 +62,6 @@ class PolicyEngine:
# Allow objects with no policies attached to pass
empty_result: bool
__pbm: PolicyBindingModel
__cached_policies: list[PolicyResult]
__processes: list[PolicyProcessInfo]
__expected_result_count: int
def __init__(
self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None
):
@ -83,8 +77,8 @@ class PolicyEngine:
self.request.obj = pbm
if request:
self.request.set_http_request(request)
self.__cached_policies = []
self.__processes = []
self.__cached_policies: list[PolicyResult] = []
self.__processes: list[PolicyProcessInfo] = []
self.use_cache = True
self.__expected_result_count = 0

View File

@ -33,21 +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
passing += passing or score <= self.threshold
LOGGER.debug(
"Score for Username",
username=request.user.username,
score=score,
passing=passing,
)
return PolicyResult(passing)
return PolicyResult(bool(passing))
class Meta:

View File

@ -51,6 +51,7 @@ class RefreshTokenModelSerializer(ExpiringBaseGrantModelSerializer):
"expires",
"scope",
"id_token",
"revoked",
]
depth = 2

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.4 on 2021-07-03 13:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_oauth2", "0014_alter_oauth2provider_rsa_key"),
]
operations = [
migrations.AddField(
model_name="authorizationcode",
name="revoked",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="refreshtoken",
name="revoked",
field=models.BooleanField(default=False),
),
]

View File

@ -278,7 +278,7 @@ class OAuth2Provider(Provider):
"""Guess launch_url based on first redirect_uri"""
if self.redirect_uris == "":
return None
main_url = self.redirect_uris.split("\n")[0]
main_url = self.redirect_uris.split("\n", maxsplit=1)[0]
launch_url = urlparse(main_url)
return main_url.replace(launch_url.path, "")
@ -318,6 +318,7 @@ class BaseGrantModel(models.Model):
provider = models.ForeignKey(OAuth2Provider, on_delete=models.CASCADE)
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
_scope = models.TextField(default="", verbose_name=_("Scopes"))
revoked = models.BooleanField(default=False)
@property
def scope(self) -> list[str]:
@ -473,9 +474,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
# Convert datetimes into timestamps.
now = int(time.time())
iat_time = now
exp_time = int(
now + timedelta_from_string(self.provider.token_validity).seconds
)
exp_time = int(dateformat.format(self.expires, "U"))
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
auth_events = Event.objects.filter(
action=EventAction.LOGIN, user=get_user(user)

View File

@ -6,6 +6,8 @@ from django.urls import reverse
from django.utils.encoding import force_str
from authentik.core.models import Application, User
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow
from authentik.providers.oauth2.constants import (
GRANT_TYPE_AUTHORIZATION_CODE,
@ -39,7 +41,8 @@ class TestToken(OAuthTestCase):
client_id=generate_client_id(),
client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid",
redirect_uris="http://testserver",
rsa_key=CertificateKeyPair.objects.first(),
)
header = b64encode(
f"{provider.client_id}:{provider.client_secret}".encode()
@ -53,11 +56,13 @@ class TestToken(OAuthTestCase):
data={
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
"code": code.code,
"redirect_uri": "http://local.invalid",
"redirect_uri": "http://testserver",
},
HTTP_AUTHORIZATION=f"Basic {header}",
)
params = TokenParams.from_request(request)
params = TokenParams.parse(
request, provider, provider.client_id, provider.client_secret
)
self.assertEqual(params.provider, provider)
def test_request_refresh_token(self):
@ -68,6 +73,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid",
rsa_key=CertificateKeyPair.objects.first(),
)
header = b64encode(
f"{provider.client_id}:{provider.client_secret}".encode()
@ -87,7 +93,9 @@ class TestToken(OAuthTestCase):
},
HTTP_AUTHORIZATION=f"Basic {header}",
)
params = TokenParams.from_request(request)
params = TokenParams.parse(
request, provider, provider.client_id, provider.client_secret
)
self.assertEqual(params.provider, provider)
def test_auth_code_view(self):
@ -98,6 +106,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid",
rsa_key=CertificateKeyPair.objects.first(),
)
# Needs to be assigned to an application for iss to be set
self.app.provider = provider
@ -141,6 +150,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid",
rsa_key=CertificateKeyPair.objects.first(),
)
# Needs to be assigned to an application for iss to be set
self.app.provider = provider
@ -193,6 +203,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid",
rsa_key=CertificateKeyPair.objects.first(),
)
header = b64encode(
f"{provider.client_id}:{provider.client_secret}".encode()
@ -230,3 +241,65 @@ class TestToken(OAuthTestCase):
),
},
)
def test_refresh_token_revoke(self):
"""test request param"""
provider = OAuth2Provider.objects.create(
name="test",
client_id=generate_client_id(),
client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(),
redirect_uris="http://testserver",
rsa_key=CertificateKeyPair.objects.first(),
)
# Needs to be assigned to an application for iss to be set
self.app.provider = provider
self.app.save()
header = b64encode(
f"{provider.client_id}:{provider.client_secret}".encode()
).decode()
user = User.objects.get(username="akadmin")
token: RefreshToken = RefreshToken.objects.create(
provider=provider,
user=user,
refresh_token=generate_client_id(),
)
# Create initial refresh token
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
data={
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
"refresh_token": token.refresh_token,
"redirect_uri": "http://testserver",
},
HTTP_AUTHORIZATION=f"Basic {header}",
)
new_token: RefreshToken = (
RefreshToken.objects.filter(user=user).exclude(pk=token.pk).first()
)
# Post again with initial token -> get new refresh token
# and revoke old one
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
data={
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
"refresh_token": new_token.refresh_token,
"redirect_uri": "http://local.invalid",
},
HTTP_AUTHORIZATION=f"Basic {header}",
)
self.assertEqual(response.status_code, 200)
# Post again with old token, is now revoked and should error
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
data={
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
"refresh_token": new_token.refresh_token,
"redirect_uri": "http://local.invalid",
},
HTTP_AUTHORIZATION=f"Basic {header}",
)
self.assertEqual(response.status_code, 400)
self.assertTrue(
Event.objects.filter(action=EventAction.SUSPICIOUS_REQUEST).exists()
)

View File

@ -10,6 +10,7 @@ from django.http.response import HttpResponseRedirect
from django.utils.cache import patch_vary_headers
from structlog.stdlib import get_logger
from authentik.events.models import Event, EventAction
from authentik.providers.oauth2.errors import BearerTokenError
from authentik.providers.oauth2.models import RefreshToken
@ -50,7 +51,7 @@ def cors_allow(request: HttpRequest, response: HttpResponse, *allowed_origins: s
if not allowed:
LOGGER.warning(
"CORS: Origin is not an allowed origin",
requested=origin,
requested=received_origin,
allowed=allowed_origins,
)
return response
@ -132,22 +133,31 @@ def protected_resource_view(scopes: list[str]):
raise BearerTokenError("invalid_token")
try:
kwargs["token"] = RefreshToken.objects.get(
token: RefreshToken = RefreshToken.objects.get(
access_token=access_token
)
except RefreshToken.DoesNotExist:
LOGGER.debug("Token does not exist", access_token=access_token)
raise BearerTokenError("invalid_token")
if kwargs["token"].is_expired:
if token.is_expired:
LOGGER.debug("Token has expired", access_token=access_token)
raise BearerTokenError("invalid_token")
if not set(scopes).issubset(set(kwargs["token"].scope)):
if token.revoked:
LOGGER.warning("Revoked token was used", access_token=access_token)
Event.new(
action=EventAction.SUSPICIOUS_REQUEST,
message="Revoked refresh token was used",
token=access_token,
).from_http(request)
raise BearerTokenError("invalid_token")
if not set(scopes).issubset(set(token.scope)):
LOGGER.warning(
"Scope missmatch.",
required=set(scopes),
token_has=set(kwargs["token"].scope),
token_has=set(token.scope),
)
raise BearerTokenError("insufficient_scope")
except BearerTokenError as error:
@ -156,7 +166,7 @@ def protected_resource_view(scopes: list[str]):
"WWW-Authenticate"
] = f'error="{error.code}", error_description="{error.description}"'
return response
kwargs["token"] = token
return view(request, *args, **kwargs)
return view_wrapper

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

@ -8,6 +8,7 @@ from django.http import HttpRequest, HttpResponse
from django.views import View
from structlog.stdlib import get_logger
from authentik.events.models import Event, EventAction
from authentik.lib.utils.time import timedelta_from_string
from authentik.providers.oauth2.constants import (
GRANT_TYPE_AUTHORIZATION_CODE,
@ -30,6 +31,7 @@ LOGGER = get_logger()
@dataclass
# pylint: disable=too-many-instance-attributes
class TokenParams:
"""Token params"""
@ -40,6 +42,8 @@ class TokenParams:
state: str
scope: list[str]
provider: OAuth2Provider
authorization_code: Optional[AuthorizationCode] = None
refresh_token: Optional[RefreshToken] = None
@ -47,35 +51,34 @@ class TokenParams:
raw_code: InitVar[str] = ""
raw_token: InitVar[str] = ""
request: InitVar[Optional[HttpRequest]] = None
@staticmethod
def from_request(request: HttpRequest) -> "TokenParams":
"""Extract Token Parameters from http request"""
client_id, client_secret = extract_client_auth(request)
def parse(
request: HttpRequest,
provider: OAuth2Provider,
client_id: str,
client_secret: str,
) -> "TokenParams":
"""Parse params for request"""
return TokenParams(
# Init vars
raw_code=request.POST.get("code", ""),
raw_token=request.POST.get("refresh_token", ""),
request=request,
# Regular params
provider=provider,
client_id=client_id,
client_secret=client_secret,
redirect_uri=request.POST.get("redirect_uri", ""),
grant_type=request.POST.get("grant_type", ""),
raw_code=request.POST.get("code", ""),
raw_token=request.POST.get("refresh_token", ""),
state=request.POST.get("state", ""),
scope=request.POST.get("scope", "").split(),
# PKCE parameter.
code_verifier=request.POST.get("code_verifier"),
)
def __post_init__(self, raw_code, raw_token):
try:
provider: OAuth2Provider = OAuth2Provider.objects.get(
client_id=self.client_id
)
self.provider = provider
except OAuth2Provider.DoesNotExist:
LOGGER.warning("OAuth2Provider does not exist", client_id=self.client_id)
raise TokenError("invalid_client")
def __post_init__(self, raw_code: str, raw_token: str, request: HttpRequest):
if self.provider.client_type == ClientTypes.CONFIDENTIAL:
if self.provider.client_secret != self.client_secret:
LOGGER.warning(
@ -87,7 +90,6 @@ class TokenParams:
if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
self.__post_init_code(raw_code)
elif self.grant_type == GRANT_TYPE_REFRESH_TOKEN:
if not raw_token:
LOGGER.warning("Missing refresh token")
@ -107,7 +109,14 @@ class TokenParams:
token=raw_token,
)
raise TokenError("invalid_grant")
if self.refresh_token.revoked:
LOGGER.warning("Refresh token is revoked", token=raw_token)
Event.new(
action=EventAction.SUSPICIOUS_REQUEST,
message="Revoked refresh token was used",
token=raw_token,
).from_http(request)
raise TokenError("invalid_grant")
else:
LOGGER.warning("Invalid grant type", grant_type=self.grant_type)
raise TokenError("unsupported_grant_type")
@ -159,13 +168,14 @@ class TokenParams:
class TokenView(View):
"""Generate tokens for clients"""
provider: Optional[OAuth2Provider] = None
params: Optional[TokenParams] = None
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
response = super().dispatch(request, *args, **kwargs)
allowed_origins = []
if self.params:
allowed_origins = self.params.provider.redirect_uris.split("\n")
if self.provider:
allowed_origins = self.provider.redirect_uris.split("\n")
cors_allow(self.request, response, *allowed_origins)
return response
@ -175,19 +185,32 @@ class TokenView(View):
def post(self, request: HttpRequest) -> HttpResponse:
"""Generate tokens for clients"""
try:
self.params = TokenParams.from_request(request)
client_id, client_secret = extract_client_auth(request)
try:
self.provider = OAuth2Provider.objects.get(client_id=client_id)
except OAuth2Provider.DoesNotExist:
LOGGER.warning(
"OAuth2Provider does not exist", client_id=self.client_id
)
raise TokenError("invalid_client")
if not self.provider:
raise ValueError
self.params = TokenParams.parse(
request, self.provider, client_id, client_secret
)
if self.params.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
return TokenResponse(self.create_code_response_dic())
return TokenResponse(self.create_code_response())
if self.params.grant_type == GRANT_TYPE_REFRESH_TOKEN:
return TokenResponse(self.create_refresh_response_dic())
return TokenResponse(self.create_refresh_response())
raise ValueError(f"Invalid grant_type: {self.params.grant_type}")
except TokenError as error:
return TokenResponse(error.create_dict(), status=400)
except UserAuthError as error:
return TokenResponse(error.create_dict(), status=403)
def create_code_response_dic(self) -> dict[str, Any]:
def create_code_response(self) -> dict[str, Any]:
"""See https://tools.ietf.org/html/rfc6749#section-4.1"""
refresh_token = self.params.authorization_code.provider.create_refresh_token(
@ -211,19 +234,19 @@ class TokenView(View):
# We don't need to store the code anymore.
self.params.authorization_code.delete()
response_dict = {
return {
"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()),
}
return response_dict
def create_refresh_response_dic(self) -> dict[str, Any]:
def create_refresh_response(self) -> dict[str, Any]:
"""See https://tools.ietf.org/html/rfc6749#section-6"""
unauthorized_scopes = set(self.params.scope) - set(
@ -251,17 +274,18 @@ class TokenView(View):
# Store the refresh_token.
refresh_token.save()
# Forget the old token.
self.params.refresh_token.delete()
# Mark old token as revoked
self.params.refresh_token.revoked = True
self.params.refresh_token.save()
dic = {
return {
"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()),
}
return dic

View File

@ -1,6 +1,7 @@
"""authentik OAuth2 OpenID Userinfo views"""
from typing import Any, Optional
from deepmerge import always_merger
from django.http import HttpRequest, HttpResponse
from django.http.response import HttpResponseBadRequest
from django.views import View
@ -78,7 +79,7 @@ class UserInfoView(View):
)
continue
LOGGER.debug("updated scope", scope=scope)
final_claims.update(value)
always_merger.merge(final_claims, value)
return final_claims
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:

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

@ -15,7 +15,7 @@ class MessageConsumer(JsonWebsocketConsumer):
cache.set(f"user_{self.session_key}_messages_{self.channel_name}", True, None)
# pylint: disable=unused-argument
def disconnect(self, close_code):
def disconnect(self, code):
cache.delete(f"user_{self.session_key}_messages_{self.channel_name}")
def event_update(self, event: dict):

View File

@ -153,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",

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

@ -36,7 +36,8 @@ class SourceType:
class SourceTypeManager:
"""Manager to hold all Source types."""
__sources: list[SourceType] = []
def __init__(self) -> None:
self.__sources: list[SourceType] = []
def type(self):
"""Class decorator to register classes inline."""

View File

@ -1,4 +1,5 @@
"""OAuth Callback Views"""
from json import JSONDecodeError
from typing import Any, Optional
from django.conf import settings
@ -10,6 +11,7 @@ from django.views.generic import View
from structlog.stdlib import get_logger
from authentik.core.sources.flow_manager import SourceFlowManager
from authentik.events.models import Event, EventAction
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.views.base import OAuthClientMixin
@ -42,8 +44,16 @@ class OAuthCallback(OAuthClientMixin, View):
if "error" in token:
return self.handle_login_failure(token["error"])
# Fetch profile info
raw_info = client.get_profile_info(token)
if raw_info is None:
try:
raw_info = client.get_profile_info(token)
if raw_info is None:
return self.handle_login_failure("Could not retrieve profile.")
except JSONDecodeError as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message="Failed to JSON-decode profile.",
raw_profile=exc.doc,
).from_http(self.request)
return self.handle_login_failure("Could not retrieve profile.")
identifier = self.get_user_id(raw_info)
if identifier is None:

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

@ -63,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

@ -74,12 +74,12 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
duo, self.stage.request, self.stage.get_pending_user()
)
def validate(self, data: dict):
def validate(self, attrs: dict):
# Checking if the given data is from a valid device class is done above
# Here we only check if the any data was sent at all
if "code" not in data and "webauthn" not in data and "duo" not in data:
if "code" not in attrs and "webauthn" not in attrs and "duo" not in attrs:
raise ValidationError("Empty response")
return data
return attrs
class AuthenticatorValidateStageView(ChallengeStageView):
@ -148,7 +148,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
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)
self.executor.plan.insert_stage(stage)
return self.executor.stage_ok()
return super().get(request, *args, **kwargs)
@ -163,7 +163,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
# pylint: disable=unused-argument
def challenge_valid(
self, challenge: AuthenticatorValidationChallengeResponse
self, response: AuthenticatorValidationChallengeResponse
) -> HttpResponse:
# All validation is done by the serializer
return self.executor.stage_ok()

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

@ -38,7 +38,7 @@ class EmailChallengeResponse(ChallengeResponse):
component = CharField(default="ak-stage-email")
def validate(self, data):
def validate(self, attrs):
raise ValidationError("")

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

@ -73,9 +73,9 @@ class IdentificationChallengeResponse(ChallengeResponse):
pre_user: Optional[User] = None
def validate(self, data: dict[str, Any]) -> dict[str, Any]:
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
"""Validate that user exists, and optionally their password"""
uid_field = data["uid_field"]
uid_field = attrs["uid_field"]
current_stage: IdentificationStage = self.stage.executor.current_stage
pre_user = self.stage.get_user(uid_field)
@ -85,13 +85,25 @@ class IdentificationChallengeResponse(ChallengeResponse):
identification_failed.send(
sender=self, request=self.stage.request, uid_field=uid_field
)
# We set the pending_user even on failure so it's part of the context, even
# when the input is invalid
# This is so its part of the current flow plan, and on flow restart can be kept, and
# policies can be applied.
self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
username=uid_field,
email=uid_field,
)
if not current_stage.show_matched_user:
self.stage.executor.plan.context[
PLAN_CONTEXT_PENDING_USER_IDENTIFIER
] = uid_field
raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user
if not current_stage.password_stage:
# No password stage select, don't validate the password
return data
return attrs
password = data["password"]
password = attrs["password"]
try:
user = authenticate(
self.stage.request,
@ -104,7 +116,7 @@ class IdentificationChallengeResponse(ChallengeResponse):
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
return data
return attrs
class IdentificationStageView(ChallengeStageView):

View File

@ -35,7 +35,9 @@ class TestUserLoginStage(TestCase):
designation=FlowDesignation.AUTHENTICATION,
)
self.stage = InvitationStage.objects.create(name="invitation")
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
@patch(
"authentik.flows.views.to_stage_response",
@ -44,7 +46,7 @@ class TestUserLoginStage(TestCase):
def test_without_invitation_fail(self):
"""Test without any invitation, continue_flow_without_invitation not set."""
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
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
@ -75,7 +77,7 @@ class TestUserLoginStage(TestCase):
self.stage.continue_flow_without_invitation = True
self.stage.save()
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
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
@ -103,7 +105,7 @@ class TestUserLoginStage(TestCase):
def test_with_invitation_get(self):
"""Test with invitation, check data in session"""
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
@ -143,7 +145,7 @@ class TestUserLoginStage(TestCase):
)
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_PROMPT] = {INVITATION_TOKEN_KEY: invite.pk.hex}
session = self.client.session

View File

@ -39,7 +39,9 @@ class TestPasswordStage(TestCase):
self.stage = PasswordStage.objects.create(
name="password", backends=[BACKEND_DJANGO]
)
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
@patch(
"authentik.flows.views.to_stage_response",
@ -48,7 +50,7 @@ class TestPasswordStage(TestCase):
def test_without_user(self):
"""Test without 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
@ -84,7 +86,7 @@ class TestPasswordStage(TestCase):
)
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
@ -101,7 +103,7 @@ class TestPasswordStage(TestCase):
def test_valid_password(self):
"""Test with a valid pending user and valid password"""
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
@ -129,7 +131,7 @@ class TestPasswordStage(TestCase):
def test_invalid_password(self):
"""Test with a valid pending user and invalid password"""
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
@ -148,7 +150,7 @@ class TestPasswordStage(TestCase):
def test_invalid_password_lockout(self):
"""Test with a valid pending user and invalid password (trigger logout counter)"""
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
@ -189,7 +191,7 @@ class TestPasswordStage(TestCase):
"""Test with a valid pending user and valid password.
Backend is patched to return PermissionError"""
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

@ -90,6 +90,14 @@ class PromptChallengeResponse(ChallengeResponse):
raise ValidationError(_("Passwords don't match."))
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
# Check if we have any static or hidden fields, and ensure they
# still have the same value
static_hidden_fields: QuerySet[Prompt] = self.stage.fields.filter(
type__in=[FieldTypes.HIDDEN, FieldTypes.STATIC]
)
for static_hidden in static_hidden_fields:
attrs[static_hidden.field_key] = static_hidden.placeholder
# Check if we have two password fields, and make sure they are the same
password_fields: QuerySet[Prompt] = self.stage.fields.filter(
type=FieldTypes.PASSWORD
@ -138,8 +146,6 @@ def password_single_validator_factory() -> Callable[[PromptChallenge, str], Any]
class ListPolicyEngine(PolicyEngine):
"""Slightly modified policy engine, which uses a list instead of a PolicyBindingModel"""
__list: list[Policy]
def __init__(
self, policies: list[Policy], user: User, request: HttpRequest = None
) -> None:

View File

@ -78,6 +78,12 @@ class TestPromptStage(TestCase):
required=True,
placeholder="HIDDEN_PLACEHOLDER",
)
static_prompt = Prompt.objects.create(
field_key="static_prompt",
type=FieldTypes.STATIC,
required=True,
placeholder="static",
)
self.stage = PromptStage.objects.create(name="prompt-stage")
self.stage.fields.set(
[
@ -88,6 +94,7 @@ class TestPromptStage(TestCase):
password2_prompt,
number_prompt,
hidden_prompt,
static_prompt,
]
)
self.stage.save()
@ -100,14 +107,17 @@ class TestPromptStage(TestCase):
password2_prompt.field_key: "test",
number_prompt.field_key: 3,
hidden_prompt.field_key: hidden_prompt.placeholder,
static_prompt.field_key: static_prompt.placeholder,
}
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_render(self):
"""Test render of form, check if all prompts are rendered correctly"""
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
@ -125,7 +135,7 @@ class TestPromptStage(TestCase):
def test_valid_challenge_with_policy(self) -> PromptChallengeResponse:
"""Test challenge_response validation"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
expr = "return request.context['password_prompt'] == request.context['password2_prompt']"
expr_policy = ExpressionPolicy.objects.create(
@ -142,7 +152,7 @@ class TestPromptStage(TestCase):
def test_invalid_challenge(self) -> PromptChallengeResponse:
"""Test challenge_response validation"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
expr = "False"
expr_policy = ExpressionPolicy.objects.create(
@ -159,7 +169,7 @@ class TestPromptStage(TestCase):
def test_valid_challenge_request(self):
"""Test a request with valid challenge_response data"""
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
@ -196,7 +206,7 @@ class TestPromptStage(TestCase):
def test_invalid_password(self):
"""Test challenge_response validation"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
self.prompt_data["password2_prompt"] = "qwerqwerqr"
challenge_response = PromptChallengeResponse(
@ -215,7 +225,7 @@ class TestPromptStage(TestCase):
def test_invalid_username(self):
"""Test challenge_response validation"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
self.prompt_data["username_prompt"] = "akadmin"
challenge_response = PromptChallengeResponse(
@ -230,3 +240,17 @@ class TestPromptStage(TestCase):
]
},
)
def test_static_hidden_overwrite(self):
"""Test that static and hidden fields ignore any value sent to them"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
self.prompt_data["hidden_prompt"] = "foo"
self.prompt_data["static_prompt"] = "foo"
challenge_response = PromptChallengeResponse(
None, stage=self.stage, plan=plan, data=self.prompt_data
)
self.assertEqual(challenge_response.is_valid(), True)
self.assertNotEqual(challenge_response.validated_data["hidden_prompt"], "foo")
self.assertNotEqual(challenge_response.validated_data["static_prompt"], "foo")

View File

@ -30,7 +30,9 @@ class TestUserDeleteStage(TestCase):
designation=FlowDesignation.AUTHENTICATION,
)
self.stage = UserDeleteStage.objects.create(name="delete")
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
@patch(
"authentik.flows.views.to_stage_response",
@ -39,7 +41,7 @@ class TestUserDeleteStage(TestCase):
def test_no_user(self):
"""Test without user set"""
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
@ -66,7 +68,7 @@ class TestUserDeleteStage(TestCase):
def test_user_delete_get(self):
"""Test Form render"""
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

@ -30,12 +30,14 @@ class TestUserLoginStage(TestCase):
designation=FlowDesignation.AUTHENTICATION,
)
self.stage = UserLoginStage.objects.create(name="login")
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()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
@ -61,7 +63,7 @@ class TestUserLoginStage(TestCase):
self.stage.session_duration = "seconds=2"
self.stage.save()
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
@ -92,7 +94,7 @@ class TestUserLoginStage(TestCase):
def test_without_user(self):
"""Test a plan without any pending user, resulting in a denied"""
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

@ -28,12 +28,14 @@ class TestUserLogoutStage(TestCase):
designation=FlowDesignation.AUTHENTICATION,
)
self.stage = UserLogoutStage.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()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO

View File

@ -12,7 +12,7 @@ class UserWriteStageSerializer(StageSerializer):
class Meta:
model = UserWriteStage
fields = StageSerializer.Meta.fields
fields = StageSerializer.Meta.fields + ["create_users_as_inactive"]
class UserWriteStageViewSet(UsedByMixin, ModelViewSet):

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.4 on 2021-06-28 20:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_user_write", "0002_auto_20200918_1653"),
]
operations = [
migrations.AddField(
model_name="userwritestage",
name="create_users_as_inactive",
field=models.BooleanField(
default=False,
help_text="When set, newly created users are inactive and cannot login.",
),
),
]

View File

@ -1,6 +1,7 @@
"""write stage models"""
from typing import Type
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
@ -12,6 +13,11 @@ class UserWriteStage(Stage):
"""Writes currently pending data into the pending user, or if no user exists,
creates a new user with the data."""
create_users_as_inactive = models.BooleanField(
default=False,
help_text=_("When set, newly created users are inactive and cannot login."),
)
@property
def serializer(self) -> BaseSerializer:
from authentik.stages.user_write.api import UserWriteStageSerializer

View File

@ -24,6 +24,10 @@ LOGGER = get_logger()
class UserWriteStageView(StageView):
"""Finalise Enrollment flow by creating a user object."""
def post(self, request: HttpRequest) -> HttpResponse:
"""Wrapper for post requests"""
return self.get(request)
def get(self, request: HttpRequest) -> HttpResponse:
"""Save data in the current flow to the currently pending user. If no user is pending,
a new user is created."""
@ -35,7 +39,9 @@ class UserWriteStageView(StageView):
data = self.executor.plan.context[PLAN_CONTEXT_PROMPT]
user_created = False
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User()
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
is_active=not self.executor.current_stage.create_users_as_inactive
)
self.executor.plan.context[
PLAN_CONTEXT_AUTHENTICATION_BACKEND
] = class_to_path(ModelBackend)

View File

@ -37,7 +37,9 @@ class TestUserWriteStage(TestCase):
designation=FlowDesignation.AUTHENTICATION,
)
self.stage = UserWriteStage.objects.create(name="write")
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
self.source = Source.objects.create(name="fake_source")
def test_user_create(self):
@ -48,7 +50,7 @@ class TestUserWriteStage(TestCase):
)
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_PROMPT] = {
"username": "test-user",
@ -92,7 +94,7 @@ class TestUserWriteStage(TestCase):
for _ in range(8)
)
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] = User.objects.create(
username="unittest", email="test@beryju.org"
@ -135,7 +137,7 @@ class TestUserWriteStage(TestCase):
def test_without_data(self):
"""Test without data results in error"""
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
@ -167,7 +169,7 @@ class TestUserWriteStage(TestCase):
def test_blank_username(self):
"""Test with blank username results in error"""
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
plan.context[PLAN_CONTEXT_PROMPT] = {
@ -204,7 +206,7 @@ class TestUserWriteStage(TestCase):
def test_duplicate_data(self):
"""Test with duplicate data, should trigger error"""
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
plan.context[PLAN_CONTEXT_PROMPT] = {

View File

@ -54,6 +54,9 @@ class CurrentTenantSerializer(PassiveSerializer):
default=CONFIG.y("footer_links", []),
)
flow_authentication = CharField(source="flow_authentication.slug", required=False)
flow_invalidation = CharField(source="flow_invalidation.slug", required=False)
flow_recovery = CharField(source="flow_recovery.slug", required=False)
flow_unenrollment = CharField(source="flow_unenrollment.slug", required=False)

View File

@ -20,6 +20,8 @@ class TestTenants(TestCase):
"branding_title": "authentik",
"matched_domain": "authentik-default",
"ui_footer_links": CONFIG.y("footer_links"),
"flow_authentication": "default-authentication-flow",
"flow_invalidation": "default-invalidation-flow",
},
)

View File

@ -21,7 +21,7 @@ services:
networks:
- internal
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.4}
restart: unless-stopped
command: server
environment:
@ -44,7 +44,7 @@ services:
- "0.0.0.0:9000:9000"
- "0.0.0.0:9443:9443"
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.4}
restart: unless-stopped
command: worker
networks:

View File

@ -1,3 +1,3 @@
package constants
const VERSION = "2021.6.2"
const VERSION = "2021.6.4"

View File

@ -10,15 +10,19 @@ import (
func (ws *WebServer) configureStatic() {
statRouter := ws.lh.NewRoute().Subrouter()
// Media files, always local
fs := http.FileServer(http.Dir(config.G.Paths.Media))
if config.G.Debug || config.G.Web.LoadLocalFiles {
ws.log.Debug("Using local static files")
ws.lh.PathPrefix("/static/dist").Handler(http.StripPrefix("/static/dist", http.FileServer(http.Dir("./web/dist"))))
ws.lh.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static/authentik", http.FileServer(http.Dir("./web/authentik"))))
statRouter.PathPrefix("/static/dist").Handler(http.StripPrefix("/static/dist", http.FileServer(http.Dir("./web/dist"))))
statRouter.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static/authentik", http.FileServer(http.Dir("./web/authentik"))))
statRouter.PathPrefix("/media").Handler(http.StripPrefix("/media", fs))
} else {
statRouter.Use(ws.staticHeaderMiddleware)
ws.log.Debug("Using packaged static files with aggressive caching")
ws.lh.PathPrefix("/static/dist").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticDist))))
ws.lh.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticAuthentik))))
statRouter.PathPrefix("/static/dist").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticDist))))
statRouter.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticAuthentik))))
statRouter.PathPrefix("/media").Handler(http.StripPrefix("/media", fs))
}
ws.lh.Path("/robots.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header()["Content-Type"] = []string{"text/plain"}
@ -30,8 +34,6 @@ func (ws *WebServer) configureStatic() {
rw.WriteHeader(200)
rw.Write(staticWeb.SecurityTxt)
})
// Media files, always local
ws.lh.PathPrefix("/media").Handler(http.StripPrefix("/media", http.FileServer(http.Dir(config.G.Paths.Media))))
}
func (ws *WebServer) staticHeaderMiddleware(h http.Handler) http.Handler {

View File

@ -98,20 +98,15 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
},
}
if *u.IsActive {
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"inactive"}})
} else {
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"active"}})
}
if u.IsSuperuser {
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"inactive"}})
} else {
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"active"}})
}
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: pi.GroupsForUser(u)})
// Old fields for backwards compatibility
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{BoolToString(*u.IsActive)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{BoolToString(u.IsSuperuser)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/active", Values: []string{BoolToString(*u.IsActive)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/superuser", Values: []string{BoolToString(u.IsSuperuser)}})
attrs = append(attrs, AKAttrsToLDAP(u.Attributes)...)
dn := fmt.Sprintf("cn=%s,%s", u.Username, pi.UserDN)

View File

@ -7,6 +7,13 @@ import (
"goauthentik.io/outpost/api"
)
func BoolToString(in bool) string {
if in {
return "true"
}
return "false"
}
func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute {
attrList := []*ldap.EntryAttribute{}
a := attrs.(*map[string]interface{})
@ -17,6 +24,8 @@ func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute {
entry.Values = t
case string:
entry.Values = []string{t}
case bool:
entry.Values = []string{BoolToString(t)}
}
attrList = append(attrList, entry)
}

View File

@ -29,9 +29,10 @@ func (s *Server) bundleProviders(providers []api.ProxyOutpostConfig) []*provider
log.WithError(err).Warning("Failed to parse URL, skipping provider")
}
bundles[idx] = &providerBundle{
s: s,
Host: externalHost.Host,
log: log.WithField("logger", "authentik.outpost.proxy-bundle").WithField("provider", provider.Name),
s: s,
Host: externalHost.Host,
log: log.WithField("logger", "authentik.outpost.proxy-bundle").WithField("provider", provider.Name),
endSessionUrl: provider.OidcConfiguration.EndSessionEndpoint,
}
bundles[idx].Build(provider)
}

View File

@ -25,6 +25,8 @@ type providerBundle struct {
proxy *OAuthProxy
Host string
endSessionUrl string
cert *tls.Certificate
log *log.Entry
@ -58,6 +60,8 @@ func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.
providerOpts.RedeemURL = provider.OidcConfiguration.TokenEndpoint
providerOpts.OIDCJwksURL = provider.OidcConfiguration.JwksUri
providerOpts.ProfileURL = provider.OidcConfiguration.UserinfoEndpoint
providerOpts.ValidateURL = provider.OidcConfiguration.UserinfoEndpoint
providerOpts.AcrValues = "goauthentik.io/providers/oauth2/default"
if *provider.SkipPathRegex != "" {
skipRegexes := strings.Split(*provider.SkipPathRegex, "\n")
@ -153,6 +157,7 @@ func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) {
oauthproxy.BasicAuthPasswordAttribute = *provider.BasicAuthPasswordAttribute
}
oauthproxy.endSessionEndpoint = pb.endSessionUrl
oauthproxy.ExternalHost = pb.Host
pb.proxy = oauthproxy

View File

@ -65,31 +65,33 @@ type OAuthProxy struct {
AuthOnlyPath string
UserInfoPath string
endSessionEndpoint string
mode api.ProxyMode
redirectURL *url.URL // the url to receive requests at
whitelistDomains []string
provider providers.Provider
sessionStore sessionsapi.SessionStore
ProxyPrefix string
serveMux http.Handler
SetXAuthRequest bool
SetBasicAuth bool
PassUserHeaders bool
BasicAuthUserAttribute string
BasicAuthPasswordAttribute string
ExternalHost string
PassAccessToken bool
SetAuthorization bool
PassAuthorization bool
PreferEmailToUser bool
skipAuthRegex []string
skipAuthPreflight bool
skipAuthStripHeaders bool
mainJwtBearerVerifier *oidc.IDTokenVerifier
extraJwtBearerVerifiers []*oidc.IDTokenVerifier
compiledRegex []*regexp.Regexp
templates *template.Template
realClientIPParser ipapi.RealClientIPParser
redirectURL *url.URL // the url to receive requests at
whitelistDomains []string
provider providers.Provider
sessionStore sessionsapi.SessionStore
ProxyPrefix string
serveMux http.Handler
SetXAuthRequest bool
SetBasicAuth bool
PassUserHeaders bool
PassAccessToken bool
SetAuthorization bool
PassAuthorization bool
PreferEmailToUser bool
skipAuthRegex []string
skipAuthPreflight bool
skipAuthStripHeaders bool
mainJwtBearerVerifier *oidc.IDTokenVerifier
extraJwtBearerVerifiers []*oidc.IDTokenVerifier
compiledRegex []*regexp.Regexp
templates *template.Template
realClientIPParser ipapi.RealClientIPParser
sessionChain alice.Chain
@ -285,19 +287,13 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
// SignOut sends a response to clear the authentication cookie
func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
redirect, err := p.GetRedirect(req)
if err != nil {
p.logger.Errorf("Error obtaining redirect: %v", err)
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
return
}
err = p.ClearSessionCookie(rw, req)
err := p.ClearSessionCookie(rw, req)
if err != nil {
p.logger.Errorf("Error clearing session cookie: %v", err)
p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
return
}
http.Redirect(rw, req, redirect, http.StatusFound)
http.Redirect(rw, req, p.endSessionEndpoint, http.StatusFound)
}
// AuthenticateOnly checks whether the user is currently logged in

View File

@ -5,7 +5,7 @@ import (
"os"
)
const VERSION = "2021.6.2"
const VERSION = "2021.6.4"
func BUILD() string {
build := os.Getenv("GIT_BUILD_HASH")

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2021.6.1
version: 2021.6.4
description: Making authentication simple.
contact:
email: hello@beryju.org
@ -3096,7 +3096,11 @@ paths:
$ref: '#/components/schemas/Link'
description: ''
'404':
description: No recovery flow found.
content:
application/json:
schema:
$ref: '#/components/schemas/Link'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
@ -3572,6 +3576,37 @@ paths:
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
post:
operationId: events_events_create
description: Event Read-Only Viewset
tags:
- events
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/EventRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/EventRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/EventRequest'
required: true
security:
- authentik: []
- cookieAuth: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/Event'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/api/v2beta/events/events/{event_uuid}/:
get:
operationId: events_events_retrieve
@ -3600,6 +3635,106 @@ paths:
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
put:
operationId: events_events_update
description: Event Read-Only Viewset
parameters:
- in: path
name: event_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Event.
required: true
tags:
- events
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/EventRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/EventRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/EventRequest'
required: true
security:
- authentik: []
- cookieAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Event'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
patch:
operationId: events_events_partial_update
description: Event Read-Only Viewset
parameters:
- in: path
name: event_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Event.
required: true
tags:
- events
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedEventRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/PatchedEventRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/PatchedEventRequest'
security:
- authentik: []
- cookieAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Event'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
delete:
operationId: events_events_destroy
description: Event Read-Only Viewset
parameters:
- in: path
name: event_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Event.
required: true
tags:
- events
security:
- authentik: []
- cookieAuth: []
responses:
'204':
description: No response body
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/api/v2beta/events/events/actions/:
get:
operationId: events_events_actions_list
@ -4441,6 +4576,18 @@ paths:
schema:
type: string
format: uuid
- in: query
name: invalid_response_action
schema:
type: string
enum:
- restart
- restart_with_context
- retry
description: 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.
- in: query
name: order
schema:
@ -18494,7 +18641,10 @@ components:
title: Kp uuid
name:
type: string
fingerprint:
fingerprint_sha256:
type: string
readOnly: true
fingerprint_sha1:
type: string
readOnly: true
cert_expiry:
@ -18517,7 +18667,8 @@ components:
- cert_expiry
- cert_subject
- certificate_download_url
- fingerprint
- fingerprint_sha1
- fingerprint_sha256
- name
- pk
- private_key_available
@ -18759,6 +18910,12 @@ components:
name: Documentation
- href: https://goauthentik.io/
name: authentik Website
flow_authentication:
type: string
flow_invalidation:
type: string
flow_recovery:
type: string
flow_unenrollment:
type: string
required:
@ -19242,7 +19399,7 @@ components:
type: object
additionalProperties: {}
action:
type: string
$ref: '#/components/schemas/EventActions'
app:
type: string
context:
@ -19266,6 +19423,34 @@ components:
- app
- created
- pk
EventActions:
enum:
- login
- login_failed
- logout
- user_write
- suspicious_request
- password_set
- secret_view
- invitation_used
- authorize_application
- source_linked
- impersonation_started
- impersonation_ended
- policy_execution
- policy_exception
- property_mapping_exception
- system_task_execution
- system_task_exception
- system_exception
- configuration_error
- model_created
- model_updated
- model_deleted
- email_sent
- update_available
- custom_
type: string
EventMatcherPolicy:
type: object
description: Event Matcher Policy Serializer
@ -19296,7 +19481,7 @@ components:
readOnly: true
action:
allOf:
- $ref: '#/components/schemas/EventMatcherPolicyActionEnum'
- $ref: '#/components/schemas/EventActions'
description: Match created events with this action type. When left empty,
all action types will be matched.
client_ip:
@ -19314,34 +19499,6 @@ components:
- pk
- verbose_name
- verbose_name_plural
EventMatcherPolicyActionEnum:
enum:
- login
- login_failed
- logout
- user_write
- suspicious_request
- password_set
- secret_view
- invitation_used
- authorize_application
- source_linked
- impersonation_started
- impersonation_ended
- policy_execution
- policy_exception
- property_mapping_exception
- system_task_execution
- system_task_exception
- system_exception
- configuration_error
- model_created
- model_updated
- model_deleted
- email_sent
- update_available
- custom_
type: string
EventMatcherPolicyRequest:
type: object
description: Event Matcher Policy Serializer
@ -19355,7 +19512,7 @@ components:
will be logged. By default, only execution errors are logged.
action:
allOf:
- $ref: '#/components/schemas/EventMatcherPolicyActionEnum'
- $ref: '#/components/schemas/EventActions'
description: Match created events with this action type. When left empty,
all action types will be matched.
client_ip:
@ -19375,7 +19532,7 @@ components:
type: object
additionalProperties: {}
action:
type: string
$ref: '#/components/schemas/EventActions'
app:
type: string
context:
@ -19673,6 +19830,13 @@ components:
minimum: -2147483648
policy_engine_mode:
$ref: '#/components/schemas/PolicyEngineMode'
invalid_response_action:
allOf:
- $ref: '#/components/schemas/InvalidResponseActionEnum'
description: 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.
required:
- order
- pk
@ -19703,6 +19867,13 @@ components:
minimum: -2147483648
policy_engine_mode:
$ref: '#/components/schemas/PolicyEngineMode'
invalid_response_action:
allOf:
- $ref: '#/components/schemas/InvalidResponseActionEnum'
description: 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.
required:
- order
- stage
@ -20048,6 +20219,12 @@ components:
- api
- recovery
type: string
InvalidResponseActionEnum:
enum:
- retry
- restart
- restart_with_context
type: string
Invitation:
type: object
description: Invitation Serializer
@ -24429,7 +24606,7 @@ components:
will be logged. By default, only execution errors are logged.
action:
allOf:
- $ref: '#/components/schemas/EventMatcherPolicyActionEnum'
- $ref: '#/components/schemas/EventActions'
description: Match created events with this action type. When left empty,
all action types will be matched.
client_ip:
@ -24441,6 +24618,29 @@ components:
- $ref: '#/components/schemas/AppEnum'
description: Match events created by selected application. When left empty,
all applications are matched.
PatchedEventRequest:
type: object
description: Event Serializer
properties:
user:
type: object
additionalProperties: {}
action:
$ref: '#/components/schemas/EventActions'
app:
type: string
context:
type: object
additionalProperties: {}
client_ip:
type: string
nullable: true
expires:
type: string
format: date-time
tenant:
type: object
additionalProperties: {}
PatchedExpressionPolicyRequest:
type: object
description: Group Membership Policy Serializer
@ -24502,6 +24702,13 @@ components:
minimum: -2147483648
policy_engine_mode:
$ref: '#/components/schemas/PolicyEngineMode'
invalid_response_action:
allOf:
- $ref: '#/components/schemas/InvalidResponseActionEnum'
description: 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.
PatchedGroupRequest:
type: object
description: Group Serializer
@ -25579,6 +25786,9 @@ components:
type: array
items:
$ref: '#/components/schemas/FlowRequest'
create_users_as_inactive:
type: boolean
description: When set, newly created users are inactive and cannot login.
PatchedWebAuthnDeviceRequest:
type: object
description: Serializer for WebAuthn authenticator devices
@ -26481,6 +26691,8 @@ components:
id_token:
type: string
readOnly: true
revoked:
type: boolean
required:
- id_token
- is_expired
@ -28073,6 +28285,9 @@ components:
type: array
items:
$ref: '#/components/schemas/Flow'
create_users_as_inactive:
type: boolean
description: When set, newly created users are inactive and cannot login.
required:
- component
- name
@ -28089,6 +28304,9 @@ components:
type: array
items:
$ref: '#/components/schemas/FlowRequest'
create_users_as_inactive:
type: boolean
description: When set, newly created users are inactive and cannot login.
required:
- name
ValidationError:

View File

@ -0,0 +1,232 @@
"""LDAP and Outpost e2e tests"""
from sys import platform
from time import sleep
from unittest.case import skipUnless
from docker.client import DockerClient, from_env
from docker.models.containers import Container
from guardian.shortcuts import get_anonymous_user
from ldap3 import (
ALL,
ALL_ATTRIBUTES,
ALL_OPERATIONAL_ATTRIBUTES,
SUBTREE,
Connection,
Server,
)
from ldap3.core.exceptions import LDAPInsufficientAccessRightsResult
from authentik.core.models import Application, Group, User
from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow
from authentik.outposts.models import Outpost, OutpostType
from authentik.providers.ldap.models import LDAPProvider
from tests.e2e.utils import (
USER,
SeleniumTestCase,
apply_migration,
object_manager,
retry,
)
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestProviderLDAP(SeleniumTestCase):
"""LDAP and Outpost e2e tests"""
ldap_container: Container
def tearDown(self) -> None:
super().tearDown()
self.output_container_logs(self.ldap_container)
self.ldap_container.kill()
def start_ldap(self, outpost: Outpost) -> Container:
"""Start ldap container based on outpost created"""
client: DockerClient = from_env()
container = client.containers.run(
image="beryju.org/authentik/outpost-ldap:gh-master",
detach=True,
network_mode="host",
auto_remove=True,
environment={
"AUTHENTIK_HOST": self.live_server_url,
"AUTHENTIK_TOKEN": outpost.token.key,
},
)
return container
def _prepare(self) -> User:
"""prepare user, provider, app and container"""
# set additionalHeaders to test later
user = USER()
user.attributes["extraAttribute"] = "bar"
user.save()
ldap: LDAPProvider = LDAPProvider.objects.create(
name="ldap_provider",
authorization_flow=Flow.objects.get(slug="default-authentication-flow"),
search_group=Group.objects.first(),
)
# we need to create an application to actually access the ldap
Application.objects.create(name="ldap", slug="ldap", provider=ldap)
outpost: Outpost = Outpost.objects.create(
name="ldap_outpost",
type=OutpostType.LDAP,
)
outpost.providers.add(ldap)
outpost.save()
user = outpost.user
self.ldap_container = self.start_ldap(outpost)
# Wait until outpost healthcheck succeeds
healthcheck_retries = 0
while healthcheck_retries < 50:
if len(outpost.state) > 0:
state = outpost.state[0]
if state.last_seen:
break
healthcheck_retries += 1
sleep(0.5)
return user
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@object_manager
def test_ldap_bind_success(self):
"""Test simple bind"""
self._prepare()
server = Server("ldap://localhost:3389", get_info=ALL)
_connection = Connection(
server,
raise_exceptions=True,
user=f"cn={USER().username},ou=users,DC=ldap,DC=goauthentik,DC=io",
password=USER().username,
)
_connection.bind()
self.assertTrue(
Event.objects.filter(
action=EventAction.LOGIN,
user={
"pk": USER().pk,
"email": USER().email,
"username": USER().username,
},
)
)
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@object_manager
def test_ldap_bind_fail(self):
"""Test simple bind (failed)"""
self._prepare()
server = Server("ldap://localhost:3389", get_info=ALL)
_connection = Connection(
server,
raise_exceptions=True,
user=f"cn={USER().username},ou=users,DC=ldap,DC=goauthentik,DC=io",
password=USER().username + "fqwerwqer",
)
with self.assertRaises(LDAPInsufficientAccessRightsResult):
_connection.bind()
anon = get_anonymous_user()
self.assertTrue(
Event.objects.filter(
action=EventAction.LOGIN_FAILED,
user={"pk": anon.pk, "email": anon.email, "username": anon.username},
)
)
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_core", "0009_group_is_superuser")
@apply_migration("authentik_flows", "0008_default_flows")
@object_manager
def test_ldap_bind_search(self):
"""Test simple bind + search"""
outpost_user = self._prepare()
server = Server("ldap://localhost:3389", get_info=ALL)
_connection = Connection(
server,
raise_exceptions=True,
user=f"cn={USER().username},ou=users,dc=ldap,dc=goauthentik,dc=io",
password=USER().username,
)
_connection.bind()
self.assertTrue(
Event.objects.filter(
action=EventAction.LOGIN,
user={
"pk": USER().pk,
"email": USER().email,
"username": USER().username,
},
)
)
_connection.search(
"ou=users,dc=ldap,dc=goauthentik,dc=io",
"(objectClass=user)",
search_scope=SUBTREE,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
)
response = _connection.response
# Remove raw_attributes to make checking easier
for obj in response:
del obj["raw_attributes"]
del obj["raw_dn"]
self.assertCountEqual(
response,
[
{
"dn": f"cn={outpost_user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
"attributes": {
"cn": [outpost_user.username],
"uid": [outpost_user.uid],
"name": [""],
"displayName": [""],
"mail": [""],
"objectClass": [
"user",
"organizationalPerson",
"goauthentik.io/ldap/user",
],
"memberOf": [],
"accountStatus": ["true"],
"superuser": ["false"],
"goauthentik.io/ldap/active": ["true"],
"goauthentik.io/ldap/superuser": ["false"],
"goauthentik.io/user/override-ips": ["true"],
"goauthentik.io/user/service-account": ["true"],
},
"type": "searchResEntry",
},
{
"dn": f"cn={USER().username},ou=users,dc=ldap,dc=goauthentik,dc=io",
"attributes": {
"cn": [USER().username],
"uid": [USER().uid],
"name": [USER().name],
"displayName": [USER().name],
"mail": [USER().email],
"objectClass": [
"user",
"organizationalPerson",
"goauthentik.io/ldap/user",
],
"memberOf": [
"cn=authentik Admins,ou=groups,dc=ldap,dc=goauthentik,dc=io"
],
"accountStatus": ["true"],
"superuser": ["true"],
"goauthentik.io/ldap/active": ["true"],
"goauthentik.io/ldap/superuser": ["true"],
"extraAttribute": ["bar"],
},
"type": "searchResEntry",
},
],
)

View File

@ -119,6 +119,13 @@ class TestProviderProxy(SeleniumTestCase):
self.assertIn("X-Forwarded-Preferred-Username: akadmin", full_body_text)
self.assertIn("X-Foo: bar", full_body_text)
self.driver.get("http://localhost:4180/akprox/sign_out")
sleep(2)
full_body_text = self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl"
).text
self.assertIn("You've logged out of proxy.", full_body_text)
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestProviderProxyConnect(ChannelsLiveServerTestCase):

View File

@ -62,11 +62,14 @@ class OutpostDockerTests(TestCase):
)
authentication_kp = CertificateKeyPair.objects.create(
name="docker-authentication",
# pylint: disable=consider-using-with
certificate_data=open(f"{self.ssl_folder}/client/cert.pem").read(),
# pylint: disable=consider-using-with
key_data=open(f"{self.ssl_folder}/client/key.pem").read(),
)
verification_kp = CertificateKeyPair.objects.create(
name="docker-verification",
# pylint: disable=consider-using-with
certificate_data=open(f"{self.ssl_folder}/client/ca.pem").read(),
)
self.service_connection = DockerServiceConnection.objects.create(

View File

@ -62,11 +62,14 @@ class TestProxyDocker(TestCase):
)
authentication_kp = CertificateKeyPair.objects.create(
name="docker-authentication",
# pylint: disable=consider-using-with
certificate_data=open(f"{self.ssl_folder}/client/cert.pem").read(),
# pylint: disable=consider-using-with
key_data=open(f"{self.ssl_folder}/client/key.pem").read(),
)
verification_kp = CertificateKeyPair.objects.create(
name="docker-verification",
# pylint: disable=consider-using-with
certificate_data=open(f"{self.ssl_folder}/client/ca.pem").read(),
)
self.service_connection = DockerServiceConnection.objects.create(

436
web/package-lock.json generated
View File

@ -18,28 +18,28 @@
"@lingui/cli": "^3.10.2",
"@lingui/core": "^3.10.4",
"@lingui/macro": "^3.10.2",
"@patternfly/patternfly": "^4.108.2",
"@patternfly/patternfly": "^4.115.2",
"@polymer/iron-form": "^3.0.1",
"@polymer/paper-input": "^3.2.1",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-replace": "^2.4.2",
"@rollup/plugin-typescript": "^8.2.1",
"@sentry/browser": "^6.7.2",
"@sentry/tracing": "^6.7.2",
"@types/chart.js": "^2.9.32",
"@types/codemirror": "5.60.0",
"@sentry/browser": "^6.8.0",
"@sentry/tracing": "^6.8.0",
"@types/chart.js": "^2.9.33",
"@types/codemirror": "5.60.1",
"@types/grecaptcha": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.1",
"@webcomponents/webcomponentsjs": "^2.5.0",
"authentik-api": "file:api",
"babel-plugin-macros": "^3.1.0",
"base64-js": "^1.5.1",
"chart.js": "^3.3.2",
"chart.js": "^3.4.1",
"chartjs-adapter-moment": "^1.0.0",
"codemirror": "^5.62.0",
"construct-style-sheets-polyfill": "^2.4.16",
"eslint": "^7.29.0",
"eslint": "^7.30.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.2",
"eslint-plugin-lit": "^1.5.1",
@ -48,7 +48,7 @@
"lit-html": "^1.4.1",
"moment": "^2.29.1",
"rapidoc": "^9.0.0",
"rollup": "^2.52.2",
"rollup": "^2.52.7",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-cssimport": "^1.0.2",
@ -58,15 +58,16 @@
"rollup-plugin-terser": "^7.0.2",
"ts-lit-plugin": "^1.2.1",
"tslib": "^2.3.0",
"typescript": "^4.3.4",
"typescript": "^4.3.5",
"webcomponent-qr-code": "^1.0.5",
"yaml": "^1.10.2"
}
},
"devDependencies": {}
},
"api": {
"name": "authentik-api",
"version": "0.0.1",
"dependencies": {
"version": "1.0.0",
"devDependencies": {
"typescript": "^3.6"
}
},
@ -74,6 +75,7 @@
"version": "3.9.9",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -1739,6 +1741,24 @@
"node": ">=6"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
"integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.0",
"debug": "^4.1.1",
"minimatch": "^3.0.4"
},
"engines": {
"node": ">=10.10.0"
}
},
"node_modules/@humanwhocodes/object-schema": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz",
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w=="
},
"node_modules/@jest/types": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
@ -2120,9 +2140,9 @@
}
},
"node_modules/@patternfly/patternfly": {
"version": "4.108.2",
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.108.2.tgz",
"integrity": "sha512-z0VB+1CXcH+eoClYQABwapX5FURSvm1nPr6asLWwg/Z4Wuxs0RjZpC6Gb+KRm8nGQwSAcMKZY1jLfPqVnznQnw=="
"version": "4.115.2",
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.115.2.tgz",
"integrity": "sha512-7hbJ4pRmj+rlXclD2F/UwceO6fS+9flGsgHc4eUc7NyTN2GXl6PLcqrjE2CtiKEPV90+KwsGQGJXZj8bz9HweA=="
},
"node_modules/@polymer/font-roboto": {
"version": "3.0.2",
@ -2314,13 +2334,13 @@
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="
},
"node_modules/@sentry/browser": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.7.2.tgz",
"integrity": "sha512-Lv0Ne1QcesyGAhVcQDfQa3hDPR/MhPSDTMg3xFi+LxqztchVc4w/ynzR0wCZFb8KIHpTj5SpJHfxpDhXYMtS9g==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.8.0.tgz",
"integrity": "sha512-nxa71csHlG5sMHUxI4e4xxuCWtbCv/QbBfMsYw7ncJSfCKG3yNlCVh8NJ7NS0rZW/MJUT6S6+r93zw0HetNDOA==",
"dependencies": {
"@sentry/core": "6.7.2",
"@sentry/types": "6.7.2",
"@sentry/utils": "6.7.2",
"@sentry/core": "6.8.0",
"@sentry/types": "6.8.0",
"@sentry/utils": "6.8.0",
"tslib": "^1.9.3"
},
"engines": {
@ -2333,14 +2353,14 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/core": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.7.2.tgz",
"integrity": "sha512-NTZqwN5nR94yrXmSfekoPs1mIFuKvf8esdIW/DadwSKWAdLJwQTJY9xK/8PQv+SEzd7wiitPAx+mCw2By1xiNQ==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.8.0.tgz",
"integrity": "sha512-vJzWt/znEB+JqVwtwfjkRrAYRN+ep+l070Ti8GhJnvwU4IDtVlV3T/jVNrj6rl6UChcczaJQMxVxtG5x0crlAA==",
"dependencies": {
"@sentry/hub": "6.7.2",
"@sentry/minimal": "6.7.2",
"@sentry/types": "6.7.2",
"@sentry/utils": "6.7.2",
"@sentry/hub": "6.8.0",
"@sentry/minimal": "6.8.0",
"@sentry/types": "6.8.0",
"@sentry/utils": "6.8.0",
"tslib": "^1.9.3"
},
"engines": {
@ -2353,12 +2373,12 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/hub": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.7.2.tgz",
"integrity": "sha512-05qVW6ymChJsXag4+fYCQokW3AcABIgcqrVYZUBf6GMU/Gbz5SJqpV7y1+njwWvnPZydMncP9LaDVpMKbE7UYQ==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.8.0.tgz",
"integrity": "sha512-hFrI2Ss1fTov7CH64FJpigqRxH7YvSnGeqxT9Jc1BL7nzW/vgCK+Oh2mOZbosTcrzoDv+lE8ViOnSN3w/fo+rg==",
"dependencies": {
"@sentry/types": "6.7.2",
"@sentry/utils": "6.7.2",
"@sentry/types": "6.8.0",
"@sentry/utils": "6.8.0",
"tslib": "^1.9.3"
},
"engines": {
@ -2371,12 +2391,12 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/minimal": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.7.2.tgz",
"integrity": "sha512-jkpwFv2GFHoVl5vnK+9/Q+Ea8eVdbJ3hn3/Dqq9MOLFnVK7ED6MhdHKLT79puGSFj+85OuhM5m2Q44mIhyS5mw==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.8.0.tgz",
"integrity": "sha512-MRxUKXiiYwKjp8mOQMpTpEuIby1Jh3zRTU0cmGZtfsZ38BC1JOle8xlwC4FdtOH+VvjSYnPBMya5lgNHNPUJDQ==",
"dependencies": {
"@sentry/hub": "6.7.2",
"@sentry/types": "6.7.2",
"@sentry/hub": "6.8.0",
"@sentry/types": "6.8.0",
"tslib": "^1.9.3"
},
"engines": {
@ -2389,14 +2409,14 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/tracing": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.7.2.tgz",
"integrity": "sha512-juKlI7FICKONWJFJxDxerj0A+8mNRhmtrdR+OXFqOkqSAy/QXlSFZcA/j//O19k2CfwK1BrvoMcQ/4gnffUOVg==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.8.0.tgz",
"integrity": "sha512-3gDkQnmOuOjHz5rY7BOatLEUksANU3efR8wuBa2ujsPQvoLSLFuyZpRjPPsxuUHQOqAYIbSNAoDloXECvQeHjw==",
"dependencies": {
"@sentry/hub": "6.7.2",
"@sentry/minimal": "6.7.2",
"@sentry/types": "6.7.2",
"@sentry/utils": "6.7.2",
"@sentry/hub": "6.8.0",
"@sentry/minimal": "6.8.0",
"@sentry/types": "6.8.0",
"@sentry/utils": "6.8.0",
"tslib": "^1.9.3"
},
"engines": {
@ -2409,19 +2429,19 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/types": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.7.2.tgz",
"integrity": "sha512-h21Go/PfstUN+ZV6SbwRSZVg9GXRJWdLfHoO5PSVb3TVEMckuxk8tAE57/u+UZDwX8wu+Xyon2TgsKpiWKxqUg==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.8.0.tgz",
"integrity": "sha512-PbSxqlh6Fd5thNU5f8EVYBVvX+G7XdPA+ThNb2QvSK8yv3rIf0McHTyF6sIebgJ38OYN7ZFK7vvhC/RgSAfYTA==",
"engines": {
"node": ">=6"
}
},
"node_modules/@sentry/utils": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.7.2.tgz",
"integrity": "sha512-9COL7aaBbe61Hp5BlArtXZ1o/cxli1NGONLPrVT4fMyeQFmLonhUiy77NdsW19XnvhvaA+2IoV5dg3dnFiF/og==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.8.0.tgz",
"integrity": "sha512-OYlI2JNrcWKMdvYbWNdQwR4QBVv2V0y5wK0U6f53nArv6RsyO5TzwRu5rMVSIZofUUqjoE5hl27jqnR+vpUrsA==",
"dependencies": {
"@sentry/types": "6.7.2",
"@sentry/types": "6.8.0",
"tslib": "^1.9.3"
},
"engines": {
@ -2434,9 +2454,9 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@types/chart.js": {
"version": "2.9.32",
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz",
"integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==",
"version": "2.9.33",
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz",
"integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==",
"dependencies": {
"moment": "^2.10.2"
}
@ -2451,9 +2471,9 @@
}
},
"node_modules/@types/codemirror": {
"version": "5.60.0",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.0.tgz",
"integrity": "sha512-xgzXZyCzedLRNC67/Nn8rpBtTFnAsX2C+Q/LGoH6zgcpF/LqdNHJMHEOhqT1bwUcSp6kQdOIuKzRbeW9DYhEhg==",
"version": "5.60.1",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.1.tgz",
"integrity": "sha512-yV14LQ5VvghnW0uSuCw2bEfZC6NvxHQEckl2w3dEk5l0yPGzQh14dCaWvG5KD/2l3cgFSifR+6nIUD7LDLdUTg==",
"dependencies": {
"@types/tern": "*"
}
@ -2579,12 +2599,12 @@
"integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA=="
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.0.tgz",
"integrity": "sha512-KcF6p3zWhf1f8xO84tuBailV5cN92vhS+VT7UJsPzGBm9VnQqfI9AsiMUFUCYHTYPg1uCCo+HyiDnpDuvkAMfQ==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.1.tgz",
"integrity": "sha512-9yfcNpDaNGQ6/LQOX/KhUFTR1sCKH+PBr234k6hI9XJ0VP5UqGxap0AnNwBnWFk1MNyWBylJH9ZkzBXC+5akZQ==",
"dependencies": {
"@typescript-eslint/experimental-utils": "4.28.0",
"@typescript-eslint/scope-manager": "4.28.0",
"@typescript-eslint/experimental-utils": "4.28.1",
"@typescript-eslint/scope-manager": "4.28.1",
"debug": "^4.3.1",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.1.0",
@ -2609,14 +2629,14 @@
}
},
"node_modules/@typescript-eslint/experimental-utils": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.0.tgz",
"integrity": "sha512-9XD9s7mt3QWMk82GoyUpc/Ji03vz4T5AYlHF9DcoFNfJ/y3UAclRsfGiE2gLfXtyC+JRA3trR7cR296TEb1oiQ==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.1.tgz",
"integrity": "sha512-n8/ggadrZ+uyrfrSEchx3jgODdmcx7MzVM2sI3cTpI/YlfSm0+9HEUaWw3aQn2urL2KYlWYMDgn45iLfjDYB+Q==",
"dependencies": {
"@types/json-schema": "^7.0.7",
"@typescript-eslint/scope-manager": "4.28.0",
"@typescript-eslint/types": "4.28.0",
"@typescript-eslint/typescript-estree": "4.28.0",
"@typescript-eslint/scope-manager": "4.28.1",
"@typescript-eslint/types": "4.28.1",
"@typescript-eslint/typescript-estree": "4.28.1",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0"
},
@ -2649,13 +2669,13 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.0.tgz",
"integrity": "sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.1.tgz",
"integrity": "sha512-UjrMsgnhQIIK82hXGaD+MCN8IfORS1CbMdu7VlZbYa8LCZtbZjJA26De4IPQB7XYZbL8gJ99KWNj0l6WD0guJg==",
"dependencies": {
"@typescript-eslint/scope-manager": "4.28.0",
"@typescript-eslint/types": "4.28.0",
"@typescript-eslint/typescript-estree": "4.28.0",
"@typescript-eslint/scope-manager": "4.28.1",
"@typescript-eslint/types": "4.28.1",
"@typescript-eslint/typescript-estree": "4.28.1",
"debug": "^4.3.1"
},
"engines": {
@ -2675,12 +2695,12 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz",
"integrity": "sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.1.tgz",
"integrity": "sha512-o95bvGKfss6705x7jFGDyS7trAORTy57lwJ+VsYwil/lOUxKQ9tA7Suuq+ciMhJc/1qPwB3XE2DKh9wubW8YYA==",
"dependencies": {
"@typescript-eslint/types": "4.28.0",
"@typescript-eslint/visitor-keys": "4.28.0"
"@typescript-eslint/types": "4.28.1",
"@typescript-eslint/visitor-keys": "4.28.1"
},
"engines": {
"node": "^8.10.0 || ^10.13.0 || >=11.10.1"
@ -2691,9 +2711,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.0.tgz",
"integrity": "sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.1.tgz",
"integrity": "sha512-4z+knEihcyX7blAGi7O3Fm3O6YRCP+r56NJFMNGsmtdw+NCdpG5SgNz427LS9nQkRVTswZLhz484hakQwB8RRg==",
"engines": {
"node": "^8.10.0 || ^10.13.0 || >=11.10.1"
},
@ -2703,12 +2723,12 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz",
"integrity": "sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.1.tgz",
"integrity": "sha512-GhKxmC4sHXxHGJv8e8egAZeTZ6HI4mLU6S7FUzvFOtsk7ZIDN1ksA9r9DyOgNqowA9yAtZXV0Uiap61bIO81FQ==",
"dependencies": {
"@typescript-eslint/types": "4.28.0",
"@typescript-eslint/visitor-keys": "4.28.0",
"@typescript-eslint/types": "4.28.1",
"@typescript-eslint/visitor-keys": "4.28.1",
"debug": "^4.3.1",
"globby": "^11.0.3",
"is-glob": "^4.0.1",
@ -2748,11 +2768,11 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz",
"integrity": "sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.1.tgz",
"integrity": "sha512-K4HMrdFqr9PFquPu178SaSb92CaWe2yErXyPumc8cYWxFmhgJsNY9eSePmO05j0JhBvf2Cdhptd6E6Yv9HVHcg==",
"dependencies": {
"@typescript-eslint/types": "4.28.0",
"@typescript-eslint/types": "4.28.1",
"eslint-visitor-keys": "^2.0.0"
},
"engines": {
@ -3316,9 +3336,9 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
},
"node_modules/chart.js": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.3.2.tgz",
"integrity": "sha512-H0hSO7xqTIrwxoACqnSoNromEMfXvfuVnrbuSt2TuXfBDDofbnto4zuZlRtRvC73/b37q3wGAWZyUU41QPvNbA=="
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz",
"integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g=="
},
"node_modules/chartjs-adapter-moment": {
"version": "1.0.0",
@ -3861,12 +3881,13 @@
}
},
"node_modules/eslint": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz",
"integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==",
"version": "7.30.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz",
"integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==",
"dependencies": {
"@babel/code-frame": "7.12.11",
"@eslint/eslintrc": "^0.4.2",
"@humanwhocodes/config-array": "^0.5.0",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@ -6770,9 +6791,9 @@
}
},
"node_modules/rollup": {
"version": "2.52.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.2.tgz",
"integrity": "sha512-4RlFC3k2BIHlUsJ9mGd8OO+9Lm2eDF5P7+6DNQOp5sx+7N/1tFM01kELfbxlMX3MxT6owvLB1ln4S3QvvQlbUA==",
"version": "2.52.7",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.7.tgz",
"integrity": "sha512-55cSH4CCU6MaPr9TAOyrIC+7qFCHscL7tkNsm1MBfIJRRqRbCEY0mmeFn4Wg8FKsHtEH8r389Fz38r/o+kgXLg==",
"bin": {
"rollup": "dist/bin/rollup"
},
@ -7604,9 +7625,9 @@
}
},
"node_modules/typescript": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz",
"integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==",
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -9191,6 +9212,21 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz",
"integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w=="
},
"@humanwhocodes/config-array": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
"integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
"requires": {
"@humanwhocodes/object-schema": "^1.2.0",
"debug": "^4.1.1",
"minimatch": "^3.0.4"
}
},
"@humanwhocodes/object-schema": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz",
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w=="
},
"@jest/types": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
@ -9482,9 +9518,9 @@
}
},
"@patternfly/patternfly": {
"version": "4.108.2",
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.108.2.tgz",
"integrity": "sha512-z0VB+1CXcH+eoClYQABwapX5FURSvm1nPr6asLWwg/Z4Wuxs0RjZpC6Gb+KRm8nGQwSAcMKZY1jLfPqVnznQnw=="
"version": "4.115.2",
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.115.2.tgz",
"integrity": "sha512-7hbJ4pRmj+rlXclD2F/UwceO6fS+9flGsgHc4eUc7NyTN2GXl6PLcqrjE2CtiKEPV90+KwsGQGJXZj8bz9HweA=="
},
"@polymer/font-roboto": {
"version": "3.0.2",
@ -9669,13 +9705,13 @@
}
},
"@sentry/browser": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.7.2.tgz",
"integrity": "sha512-Lv0Ne1QcesyGAhVcQDfQa3hDPR/MhPSDTMg3xFi+LxqztchVc4w/ynzR0wCZFb8KIHpTj5SpJHfxpDhXYMtS9g==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.8.0.tgz",
"integrity": "sha512-nxa71csHlG5sMHUxI4e4xxuCWtbCv/QbBfMsYw7ncJSfCKG3yNlCVh8NJ7NS0rZW/MJUT6S6+r93zw0HetNDOA==",
"requires": {
"@sentry/core": "6.7.2",
"@sentry/types": "6.7.2",
"@sentry/utils": "6.7.2",
"@sentry/core": "6.8.0",
"@sentry/types": "6.8.0",
"@sentry/utils": "6.8.0",
"tslib": "^1.9.3"
},
"dependencies": {
@ -9687,14 +9723,14 @@
}
},
"@sentry/core": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.7.2.tgz",
"integrity": "sha512-NTZqwN5nR94yrXmSfekoPs1mIFuKvf8esdIW/DadwSKWAdLJwQTJY9xK/8PQv+SEzd7wiitPAx+mCw2By1xiNQ==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.8.0.tgz",
"integrity": "sha512-vJzWt/znEB+JqVwtwfjkRrAYRN+ep+l070Ti8GhJnvwU4IDtVlV3T/jVNrj6rl6UChcczaJQMxVxtG5x0crlAA==",
"requires": {
"@sentry/hub": "6.7.2",
"@sentry/minimal": "6.7.2",
"@sentry/types": "6.7.2",
"@sentry/utils": "6.7.2",
"@sentry/hub": "6.8.0",
"@sentry/minimal": "6.8.0",
"@sentry/types": "6.8.0",
"@sentry/utils": "6.8.0",
"tslib": "^1.9.3"
},
"dependencies": {
@ -9706,12 +9742,12 @@
}
},
"@sentry/hub": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.7.2.tgz",
"integrity": "sha512-05qVW6ymChJsXag4+fYCQokW3AcABIgcqrVYZUBf6GMU/Gbz5SJqpV7y1+njwWvnPZydMncP9LaDVpMKbE7UYQ==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.8.0.tgz",
"integrity": "sha512-hFrI2Ss1fTov7CH64FJpigqRxH7YvSnGeqxT9Jc1BL7nzW/vgCK+Oh2mOZbosTcrzoDv+lE8ViOnSN3w/fo+rg==",
"requires": {
"@sentry/types": "6.7.2",
"@sentry/utils": "6.7.2",
"@sentry/types": "6.8.0",
"@sentry/utils": "6.8.0",
"tslib": "^1.9.3"
},
"dependencies": {
@ -9723,12 +9759,12 @@
}
},
"@sentry/minimal": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.7.2.tgz",
"integrity": "sha512-jkpwFv2GFHoVl5vnK+9/Q+Ea8eVdbJ3hn3/Dqq9MOLFnVK7ED6MhdHKLT79puGSFj+85OuhM5m2Q44mIhyS5mw==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.8.0.tgz",
"integrity": "sha512-MRxUKXiiYwKjp8mOQMpTpEuIby1Jh3zRTU0cmGZtfsZ38BC1JOle8xlwC4FdtOH+VvjSYnPBMya5lgNHNPUJDQ==",
"requires": {
"@sentry/hub": "6.7.2",
"@sentry/types": "6.7.2",
"@sentry/hub": "6.8.0",
"@sentry/types": "6.8.0",
"tslib": "^1.9.3"
},
"dependencies": {
@ -9740,14 +9776,14 @@
}
},
"@sentry/tracing": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.7.2.tgz",
"integrity": "sha512-juKlI7FICKONWJFJxDxerj0A+8mNRhmtrdR+OXFqOkqSAy/QXlSFZcA/j//O19k2CfwK1BrvoMcQ/4gnffUOVg==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.8.0.tgz",
"integrity": "sha512-3gDkQnmOuOjHz5rY7BOatLEUksANU3efR8wuBa2ujsPQvoLSLFuyZpRjPPsxuUHQOqAYIbSNAoDloXECvQeHjw==",
"requires": {
"@sentry/hub": "6.7.2",
"@sentry/minimal": "6.7.2",
"@sentry/types": "6.7.2",
"@sentry/utils": "6.7.2",
"@sentry/hub": "6.8.0",
"@sentry/minimal": "6.8.0",
"@sentry/types": "6.8.0",
"@sentry/utils": "6.8.0",
"tslib": "^1.9.3"
},
"dependencies": {
@ -9759,16 +9795,16 @@
}
},
"@sentry/types": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.7.2.tgz",
"integrity": "sha512-h21Go/PfstUN+ZV6SbwRSZVg9GXRJWdLfHoO5PSVb3TVEMckuxk8tAE57/u+UZDwX8wu+Xyon2TgsKpiWKxqUg=="
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.8.0.tgz",
"integrity": "sha512-PbSxqlh6Fd5thNU5f8EVYBVvX+G7XdPA+ThNb2QvSK8yv3rIf0McHTyF6sIebgJ38OYN7ZFK7vvhC/RgSAfYTA=="
},
"@sentry/utils": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.7.2.tgz",
"integrity": "sha512-9COL7aaBbe61Hp5BlArtXZ1o/cxli1NGONLPrVT4fMyeQFmLonhUiy77NdsW19XnvhvaA+2IoV5dg3dnFiF/og==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.8.0.tgz",
"integrity": "sha512-OYlI2JNrcWKMdvYbWNdQwR4QBVv2V0y5wK0U6f53nArv6RsyO5TzwRu5rMVSIZofUUqjoE5hl27jqnR+vpUrsA==",
"requires": {
"@sentry/types": "6.7.2",
"@sentry/types": "6.8.0",
"tslib": "^1.9.3"
},
"dependencies": {
@ -9780,9 +9816,9 @@
}
},
"@types/chart.js": {
"version": "2.9.32",
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz",
"integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==",
"version": "2.9.33",
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz",
"integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==",
"requires": {
"moment": "^2.10.2"
}
@ -9797,9 +9833,9 @@
}
},
"@types/codemirror": {
"version": "5.60.0",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.0.tgz",
"integrity": "sha512-xgzXZyCzedLRNC67/Nn8rpBtTFnAsX2C+Q/LGoH6zgcpF/LqdNHJMHEOhqT1bwUcSp6kQdOIuKzRbeW9DYhEhg==",
"version": "5.60.1",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.1.tgz",
"integrity": "sha512-yV14LQ5VvghnW0uSuCw2bEfZC6NvxHQEckl2w3dEk5l0yPGzQh14dCaWvG5KD/2l3cgFSifR+6nIUD7LDLdUTg==",
"requires": {
"@types/tern": "*"
}
@ -9925,12 +9961,12 @@
"integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA=="
},
"@typescript-eslint/eslint-plugin": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.0.tgz",
"integrity": "sha512-KcF6p3zWhf1f8xO84tuBailV5cN92vhS+VT7UJsPzGBm9VnQqfI9AsiMUFUCYHTYPg1uCCo+HyiDnpDuvkAMfQ==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.1.tgz",
"integrity": "sha512-9yfcNpDaNGQ6/LQOX/KhUFTR1sCKH+PBr234k6hI9XJ0VP5UqGxap0AnNwBnWFk1MNyWBylJH9ZkzBXC+5akZQ==",
"requires": {
"@typescript-eslint/experimental-utils": "4.28.0",
"@typescript-eslint/scope-manager": "4.28.0",
"@typescript-eslint/experimental-utils": "4.28.1",
"@typescript-eslint/scope-manager": "4.28.1",
"debug": "^4.3.1",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.1.0",
@ -9939,14 +9975,14 @@
}
},
"@typescript-eslint/experimental-utils": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.0.tgz",
"integrity": "sha512-9XD9s7mt3QWMk82GoyUpc/Ji03vz4T5AYlHF9DcoFNfJ/y3UAclRsfGiE2gLfXtyC+JRA3trR7cR296TEb1oiQ==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.1.tgz",
"integrity": "sha512-n8/ggadrZ+uyrfrSEchx3jgODdmcx7MzVM2sI3cTpI/YlfSm0+9HEUaWw3aQn2urL2KYlWYMDgn45iLfjDYB+Q==",
"requires": {
"@types/json-schema": "^7.0.7",
"@typescript-eslint/scope-manager": "4.28.0",
"@typescript-eslint/types": "4.28.0",
"@typescript-eslint/typescript-estree": "4.28.0",
"@typescript-eslint/scope-manager": "4.28.1",
"@typescript-eslint/types": "4.28.1",
"@typescript-eslint/typescript-estree": "4.28.1",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0"
},
@ -9962,37 +9998,37 @@
}
},
"@typescript-eslint/parser": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.0.tgz",
"integrity": "sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.1.tgz",
"integrity": "sha512-UjrMsgnhQIIK82hXGaD+MCN8IfORS1CbMdu7VlZbYa8LCZtbZjJA26De4IPQB7XYZbL8gJ99KWNj0l6WD0guJg==",
"requires": {
"@typescript-eslint/scope-manager": "4.28.0",
"@typescript-eslint/types": "4.28.0",
"@typescript-eslint/typescript-estree": "4.28.0",
"@typescript-eslint/scope-manager": "4.28.1",
"@typescript-eslint/types": "4.28.1",
"@typescript-eslint/typescript-estree": "4.28.1",
"debug": "^4.3.1"
}
},
"@typescript-eslint/scope-manager": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz",
"integrity": "sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.1.tgz",
"integrity": "sha512-o95bvGKfss6705x7jFGDyS7trAORTy57lwJ+VsYwil/lOUxKQ9tA7Suuq+ciMhJc/1qPwB3XE2DKh9wubW8YYA==",
"requires": {
"@typescript-eslint/types": "4.28.0",
"@typescript-eslint/visitor-keys": "4.28.0"
"@typescript-eslint/types": "4.28.1",
"@typescript-eslint/visitor-keys": "4.28.1"
}
},
"@typescript-eslint/types": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.0.tgz",
"integrity": "sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA=="
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.1.tgz",
"integrity": "sha512-4z+knEihcyX7blAGi7O3Fm3O6YRCP+r56NJFMNGsmtdw+NCdpG5SgNz427LS9nQkRVTswZLhz484hakQwB8RRg=="
},
"@typescript-eslint/typescript-estree": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz",
"integrity": "sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.1.tgz",
"integrity": "sha512-GhKxmC4sHXxHGJv8e8egAZeTZ6HI4mLU6S7FUzvFOtsk7ZIDN1ksA9r9DyOgNqowA9yAtZXV0Uiap61bIO81FQ==",
"requires": {
"@typescript-eslint/types": "4.28.0",
"@typescript-eslint/visitor-keys": "4.28.0",
"@typescript-eslint/types": "4.28.1",
"@typescript-eslint/visitor-keys": "4.28.1",
"debug": "^4.3.1",
"globby": "^11.0.3",
"is-glob": "^4.0.1",
@ -10016,11 +10052,11 @@
}
},
"@typescript-eslint/visitor-keys": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz",
"integrity": "sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.1.tgz",
"integrity": "sha512-K4HMrdFqr9PFquPu178SaSb92CaWe2yErXyPumc8cYWxFmhgJsNY9eSePmO05j0JhBvf2Cdhptd6E6Yv9HVHcg==",
"requires": {
"@typescript-eslint/types": "4.28.0",
"@typescript-eslint/types": "4.28.1",
"eslint-visitor-keys": "^2.0.0"
}
},
@ -10170,7 +10206,8 @@
"typescript": {
"version": "3.9.9",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w=="
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
"dev": true
}
}
},
@ -10461,9 +10498,9 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
},
"chart.js": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.3.2.tgz",
"integrity": "sha512-H0hSO7xqTIrwxoACqnSoNromEMfXvfuVnrbuSt2TuXfBDDofbnto4zuZlRtRvC73/b37q3wGAWZyUU41QPvNbA=="
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz",
"integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g=="
},
"chartjs-adapter-moment": {
"version": "1.0.0",
@ -10899,12 +10936,13 @@
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"eslint": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz",
"integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==",
"version": "7.30.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz",
"integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==",
"requires": {
"@babel/code-frame": "7.12.11",
"@eslint/eslintrc": "^0.4.2",
"@humanwhocodes/config-array": "^0.5.0",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@ -13200,9 +13238,9 @@
}
},
"rollup": {
"version": "2.52.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.2.tgz",
"integrity": "sha512-4RlFC3k2BIHlUsJ9mGd8OO+9Lm2eDF5P7+6DNQOp5sx+7N/1tFM01kELfbxlMX3MxT6owvLB1ln4S3QvvQlbUA==",
"version": "2.52.7",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.7.tgz",
"integrity": "sha512-55cSH4CCU6MaPr9TAOyrIC+7qFCHscL7tkNsm1MBfIJRRqRbCEY0mmeFn4Wg8FKsHtEH8r389Fz38r/o+kgXLg==",
"requires": {
"fsevents": "~2.3.2"
}
@ -13896,9 +13934,9 @@
}
},
"typescript": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz",
"integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew=="
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA=="
},
"uglify-js": {
"version": "3.13.0",

View File

@ -47,28 +47,28 @@
"@lingui/cli": "^3.10.2",
"@lingui/core": "^3.10.4",
"@lingui/macro": "^3.10.2",
"@patternfly/patternfly": "^4.108.2",
"@patternfly/patternfly": "^4.115.2",
"@polymer/iron-form": "^3.0.1",
"@polymer/paper-input": "^3.2.1",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-replace": "^2.4.2",
"@rollup/plugin-typescript": "^8.2.1",
"@sentry/browser": "^6.7.2",
"@sentry/tracing": "^6.7.2",
"@types/chart.js": "^2.9.32",
"@types/codemirror": "5.60.0",
"@sentry/browser": "^6.8.0",
"@sentry/tracing": "^6.8.0",
"@types/chart.js": "^2.9.33",
"@types/codemirror": "5.60.1",
"@types/grecaptcha": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.1",
"@webcomponents/webcomponentsjs": "^2.5.0",
"authentik-api": "file:api",
"babel-plugin-macros": "^3.1.0",
"base64-js": "^1.5.1",
"chart.js": "^3.3.2",
"chart.js": "^3.4.1",
"chartjs-adapter-moment": "^1.0.0",
"codemirror": "^5.62.0",
"construct-style-sheets-polyfill": "^2.4.16",
"eslint": "^7.29.0",
"eslint": "^7.30.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.2",
"eslint-plugin-lit": "^1.5.1",
@ -77,7 +77,7 @@
"lit-html": "^1.4.1",
"moment": "^2.29.1",
"rapidoc": "^9.0.0",
"rollup": "^2.52.2",
"rollup": "^2.52.7",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-cssimport": "^1.0.2",
@ -87,7 +87,7 @@
"rollup-plugin-terser": "^7.0.2",
"ts-lit-plugin": "^1.2.1",
"tslib": "^2.3.0",
"typescript": "^4.3.4",
"typescript": "^4.3.5",
"webcomponent-qr-code": "^1.0.5",
"yaml": "^1.10.2"
},

View File

@ -7,7 +7,16 @@ export class LoggingMiddleware implements Middleware {
post(context: ResponseContext): Promise<Response | void> {
tenant().then(tenant => {
console.debug(`authentik/api[${tenant.matchedDomain}]: ${context.response.status} ${context.init.method} ${context.url}`);
let msg = `authentik/api[${tenant.matchedDomain}]: `;
msg += `${context.response.status} ${context.init.method} ${context.url}`;
if (context.response.status >= 400) {
context.response.text().then(t => {
msg += ` => ${t}`;
console.debug(msg);
});
} else {
console.debug(msg);
}
});
return Promise.resolve(context.response);
}

View File

@ -139,6 +139,7 @@ body {
/* Card */
.pf-c-card {
--pf-c-card--BackgroundColor: var(--ak-dark-background-light);
color: var(--ak-dark-foreground);
}
.pf-c-card__title,
.pf-c-card__body {

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2021.6.2";
export const VERSION = "2021.6.4";
export const PAGE_SIZE = 20;
export const EVENT_REFRESH = "ak-refresh";
export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle";

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