Compare commits

..

144 Commits

Author SHA1 Message Date
fe5d22ce6c release: 2021.8.5 2021-09-10 22:10:35 +02:00
0e30b6ee55 lifecycle: fix worker startup error when docker socket's group is not called docker
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-10 22:05:00 +02:00
6cbba45291 web: ignore network error
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-10 21:51:11 +02:00
ba023a3bba outpost: update global outpost config on refresh
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-10 21:51:02 +02:00
6c805bcf32 sources/oauth: don't cancel flow when redirecting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-10 21:50:45 +02:00
bc7d5042df outpost/proxy: use common template for proxy error
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-07 16:44:15 +02:00
de3e1c3dbc sources/oauth: fix FlowExecutor view call
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-07 11:39:03 +02:00
3c6aac5435 sources/oauth: prevent potentially confidential data from being logged
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-07 11:05:18 +02:00
eeb755ab7d root: show location header in logs when redirecting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-07 11:04:00 +02:00
70d0dd51a5 sources/oauth: cancel currently active flows before redirecting out
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-07 11:03:45 +02:00
073dd8b560 web/admin: fix notification clear all not triggering render
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-07 10:23:55 +02:00
b5d2924d46 website/docs: update 2021.8.5
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-07 10:10:43 +02:00
597e279f34 ci: fix old node version in release ci
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-07 10:07:36 +02:00
fc28def83d build(deps): bump @typescript-eslint/eslint-plugin in /web (#1358) 2021-09-07 08:42:57 +02:00
f6efdfded4 build(deps): bump @typescript-eslint/parser in /web (#1357) 2021-09-07 08:31:13 +02:00
91312496e0 ci: simplify testspace setup
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-06 16:52:21 +02:00
b557b4337d build(deps): bump @babel/core from 7.15.4 to 7.15.5 in /web (#1351) 2021-09-06 08:36:40 +02:00
bfde186aa0 build(deps): bump actions/cache from 1 to 2.1.6 (#1352) 2021-09-06 08:36:32 +02:00
2bd75dd1a9 build(deps): bump xmlsec from 1.3.11 to 1.3.12 (#1353) 2021-09-06 08:36:16 +02:00
27ab31a9b0 build(deps): bump boto3 from 1.18.35 to 1.18.36 (#1354) 2021-09-06 08:35:56 +02:00
44a8b737d9 build(deps): bump drf-spectacular from 0.18.1 to 0.18.2 (#1355) 2021-09-06 08:35:45 +02:00
b939ee7a09 website/docs: use kubectl exec with deployment, add note for backup version
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1349
2021-09-05 20:25:42 +02:00
0bae550520 root: include authentik version in backup naming
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-05 20:25:02 +02:00
b5cc2f2bda website/docs: add missing ENV, changed k8s beta instructions (#1350)
* fixed IsActive and IsSuperuser return string

IsActive and IsSuperuser attributes were interchanged.

* updated docs

Co-authored-by: Tobias Mandjik <tobias.mandjik@linogics.io>
2021-09-05 19:58:42 +02:00
9ad4cf1db9 outposts/ldap: improve logging of client IPs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-05 19:47:30 +02:00
9dbafaaea2 web: Update Web API Client version (#1348)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 22:49:16 +02:00
2db8b07578 events: add mark_all_seen
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 22:08:12 +02:00
7c1a7bfd9d ci: use native kind action to test integration
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 16:06:44 +02:00
b7ef076798 outposts: add expected outpost replica count to metrics
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 15:56:57 +02:00
37c29a073e policies/password: fix symbols not being checked correctly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 15:21:48 +02:00
0c288ea64b ci: cache webui for e2e tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 15:21:24 +02:00
2476475174 ci: attempt to cache pipenv (#1347)
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 14:55:54 +02:00
71913c8164 website/docs: fix typos in vikunja docs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 14:38:35 +02:00
6ec8432217 policies/password: don't use regex for symbol detection
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 14:36:01 +02:00
7a12c0e4d1 web/admin: fix user selection in token form
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 14:33:40 +02:00
23a7eba16b website/docs: add 8.5 release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 14:09:11 +02:00
3ba84a8e8b stages/identification: fix empty user_fields query returning first user
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 14:07:14 +02:00
75476217a0 internal: fix web requests not having a logger set
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 13:52:47 +02:00
7771c0b905 internal: fix font loading errors on safari
closes #1057

for some reason safari appends the relative font path to the document URL not to the stylesheet URL. Since I don't want to build a fully custom patternfly base css file, this mounts the static files where safari expects them

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 13:50:29 +02:00
3378e82ec7 root: fix is_secure with safari on debug environments
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 13:45:50 +02:00
126e43dea4 internal: disable directory listing on static files
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 13:40:29 +02:00
f725009530 web/flows: fix display error when using IdentificationStage without input fields
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-04 13:06:37 +02:00
70d1e3a0cb outpost: fix spans being sent without parent context
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-03 18:17:08 +02:00
e751ce1220 root: update badges
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-03 18:11:13 +02:00
e09a27cf87 events: remove authentik_events gauge
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-03 18:04:26 +02:00
06fbf44724 root: update security.md
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-03 17:26:10 +02:00
200e409d91 core: minor query optimization
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-03 14:02:57 +02:00
5e5854e256 ci: fix invalid workflow
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-03 10:58:42 +02:00
3df8bcfc9c web: Update Web API Client version (#1345)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-09-03 10:53:59 +02:00
e76c14f9e0 ci: run on pr and improve checking for push
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-03 10:53:39 +02:00
6b6748b1c7 web/admin: show applications instead of providers in outpost form
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-03 10:43:21 +02:00
d92d8e6dbb api: add additional filters for ldap and proxy providers
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-03 10:43:09 +02:00
c2b9dc5c75 api: cache schema, fix server urls
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-03 10:23:14 +02:00
5c1d27de2b build(deps): bump docker from 5.0.1 to 5.0.2 (#1343) 2021-09-03 08:46:33 +02:00
6ab9e7cd68 build(deps): bump @babel/core from 7.15.0 to 7.15.4 in /web (#1339) 2021-09-03 08:46:23 +02:00
3ef56e9ec1 build(deps): bump @docusaurus/plugin-client-redirects in /website (#1338) 2021-09-03 08:46:05 +02:00
6d8d157772 build(deps): bump @babel/plugin-proposal-decorators in /web (#1340) 2021-09-03 08:44:36 +02:00
cadd466eec build(deps): bump @docusaurus/preset-classic in /website (#1341) 2021-09-03 08:44:27 +02:00
3fea0c1e49 build(deps): bump @babel/preset-env from 7.15.0 to 7.15.4 in /web (#1342) 2021-09-03 08:44:16 +02:00
4c58201adc build(deps): bump boto3 from 1.18.34 to 1.18.35 (#1344) 2021-09-03 08:44:02 +02:00
4fb4e72624 web: Update Web API Client version (#1337)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-09-02 21:34:04 +02:00
276d8fe5cf release: 2021.8.4 2021-09-02 20:21:21 +02:00
92ce5f0931 web: improve error display when only {'detail'} is returned
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-02 19:55:37 +02:00
7fea20375f *: fix tests not using APITestCase
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-02 19:14:21 +02:00
d4d4034d2c web: Update Web API Client version (#1336)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-09-02 17:42:55 +02:00
f0db408699 api: add v3
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-02 17:40:02 +02:00
5e200655d9 web: Update Web API Client version (#1335)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-09-02 17:13:16 +02:00
d5d1f2a645 web: show version in logs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-02 17:10:43 +02:00
cc5cc43baa api: fix sentry endpoint not working due to mime-media
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-02 16:56:53 +02:00
e512f085db root: allow enabling s3 backup ssl verification
closes #1332

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-02 09:41:55 +02:00
f323c01bd8 build(deps): bump django from 3.2.6 to 3.2.7 (#1333) 2021-09-02 09:12:24 +02:00
f56cacb406 build(deps): bump boto3 from 1.18.33 to 1.18.34 (#1334) 2021-09-02 09:12:03 +02:00
eaecd31e9f ci: always run codecov and testspace
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 22:59:51 +02:00
36989d82e1 ci: merge on testspace
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 22:35:17 +02:00
50777d9022 ci: re-add testspace (#1331)
* ci: re-add testspace

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

* ci: fix double k3d

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 22:33:10 +02:00
a15571bd3e outposts/proxy: detect empty authentik_host
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 22:09:07 +02:00
26fd66d831 stages/authenticator_validate: fix variable shadowing, optimization
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 19:54:54 +02:00
0be873025a ci: fix bumpversion path
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 19:38:04 +02:00
28ada49910 website/docs: final 2021.8.4 release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 19:37:00 +02:00
4fc8e61f8c stages/authenticator_validate: show single button for multiple webauthn authenticators
tested with browser + yubikey 5

closes #1096

The order of allowCredentials doesn't seem to matter, chrome seems to always choose the internal authenticator first.

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 19:28:52 +02:00
7d26ea1a9c web/admin: fix list of webauthn devices not updating after rename
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 19:05:18 +02:00
3a58dc62e1 ci: fix missing branch
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 18:34:57 +02:00
71fe7bc827 ci: fix sha being used instead of timestamp
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 17:10:42 +02:00
933336c38b ci: fix images not being pushed with correct tags
* ci: debug

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

* ci: fix branch and sha

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 16:19:29 +02:00
371feb9a31 ci: fix images not being pushed
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 15:07:13 +02:00
95a2fd3c9e web: Update Web API Client version (#1327)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-09-01 14:48:48 +02:00
17cb76c334 stages/invitation: fix invitation not inheriting ExpiringModel
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 14:25:19 +02:00
88f0dfc8cc web/admin: fallback for invitation list on first load
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 13:33:05 +02:00
f82aada23b web/admin: fix flow executor not opening in new tab
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-09-01 13:19:09 +02:00
ecaee92634 build(deps): bump @sentry/tracing from 6.11.0 to 6.12.0 in /web (#1322)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 6.11.0 to 6.12.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.11.0...6.12.0)

---
updated-dependencies:
- dependency-name: "@sentry/tracing"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-01 09:26:53 +02:00
89252ec47b build(deps): bump @sentry/tracing from 6.11.0 to 6.12.0 in /website (#1320)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 6.11.0 to 6.12.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.11.0...6.12.0)

---
updated-dependencies:
- dependency-name: "@sentry/tracing"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-01 09:26:33 +02:00
f0f25ab291 build(deps): bump @sentry/react from 6.11.0 to 6.12.0 in /website (#1321) 2021-09-01 08:40:07 +02:00
e4d0fec15a build(deps): bump @sentry/browser from 6.11.0 to 6.12.0 in /web (#1323) 2021-09-01 08:39:56 +02:00
6b10baf086 build(deps): bump docker from 5.0.0 to 5.0.1 (#1324) 2021-09-01 08:39:21 +02:00
f148b5d341 build(deps): bump boto3 from 1.18.32 to 1.18.33 (#1326) 2021-09-01 08:39:12 +02:00
1471ff8940 build(deps): bump drf-spectacular from 0.18.0 to 0.18.1 (#1325) 2021-09-01 08:39:01 +02:00
d9a6ec2ac0 webiste/docs: update extensionvs/v1beta ingress
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-31 21:11:01 +02:00
5745ffa0a8 ci: don't login to docker on forks
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-31 09:31:10 +02:00
b26202db35 build(deps): bump @typescript-eslint/parser in /web (#1316) 2021-08-31 08:42:14 +02:00
6318577a51 build(deps): bump @typescript-eslint/eslint-plugin in /web (#1317) 2021-08-31 08:16:59 +02:00
6a2cd45847 build(deps-dev): bump pytest from 6.2.4 to 6.2.5 (#1318) 2021-08-31 08:16:44 +02:00
ef5cea2c01 build(deps): bump boto3 from 1.18.31 to 1.18.32 (#1319) 2021-08-31 08:16:32 +02:00
69f4d54bae ci: migrate ci to gh actions (#1315) 2021-08-30 20:21:15 +02:00
b1eec5a7d2 outposts/proxy: add more logging
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-30 17:18:52 +02:00
1b8271d767 flows: disable compatibility_mode by default
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-30 17:18:43 +02:00
3e9f5ec5ef providers/proxy: improve error handling for non-tls ingresses
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-30 14:43:57 +02:00
63f57b6a77 events: improve logging for task exceptions
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-30 14:43:44 +02:00
a016f99450 core: fix user_obj being empty on token API
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-30 12:51:17 +02:00
adc18b2991 build(deps): bump boto3 from 1.18.30 to 1.18.31 (#1314)
Bumps [boto3](https://github.com/boto/boto3) from 1.18.30 to 1.18.31.
- [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.18.30...1.18.31)

---
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-08-30 09:12:58 +02:00
e37a326b95 website/docs: prepare 8.4 docs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-29 22:12:49 +02:00
048467e97d outpost/ldap: delay user information removal upon closing of connection
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-29 21:13:46 +02:00
cc2cd6919f outpost/embedded: only send requests for non-akprox paths when we're doing proxy mode
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-29 21:13:28 +02:00
0c6e781e5b providers/proxy: fix traefik middleware being generated with wrong ports for embedded outposts
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-29 20:49:11 +02:00
7294d8fca5 website/docs: add note for cross-namespace reference in traefik
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-29 20:46:17 +02:00
16ec5680b4 web: Update Web API Client version (#1313)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-08-29 19:51:10 +02:00
87920fb1d7 website/docs: add docs for websocket connections
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-29 19:49:18 +02:00
523b96a6d2 api: add basic rate limiting for sentry endpoint
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-29 19:33:18 +02:00
45731d8069 cmd: add option to disable embedded outpost
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-29 19:19:13 +02:00
e872371970 website/docs: add embedded outpost docs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-29 14:43:13 +02:00
08e8cf850a web/flows: fix FlowExecutor not updating when challenge changes from outside
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-29 13:49:57 +02:00
b1ed2154ac policies/password: fix PasswordStage not being usable with prompt stages, rework validation logic
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-29 00:40:36 +02:00
7ef2aa3eb9 web: Update Web API Client version (#1312) 2021-08-28 19:08:38 +02:00
160139813d release: 2021.8.3 2021-08-28 16:58:44 +02:00
582ad92c76 outposts/k8s: improve error handling
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-28 14:58:26 +02:00
f61736e3d1 stages/identification: add error handling when password isn't set
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-28 12:54:10 +02:00
eb02c96281 website/docs: make it clearer to use context[]
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-28 12:53:57 +02:00
8619552920 website/docs: prepare 2021.8.3
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-27 23:12:53 +02:00
6237352e25 web/flows: fix checkboxes not being rendered correctly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-27 23:09:53 +02:00
2d8b4f543b providers/proxy: fix url parsing for traefik labels on docker containers
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-27 22:21:16 +02:00
8542dc10ab providers/proxy: fix docker container labels not being inherited correctly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-27 20:20:34 +02:00
c55b63337c web/flows: fix post-challenge updates not always being called by using setter
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-27 19:45:23 +02:00
12ddee3bb6 outpost: add additional labels to docker container
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-27 19:26:27 +02:00
dc41d0af27 outposts: add configurable docker_network for outpost
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-27 19:26:11 +02:00
3323b50036 web/flows: also check for redirects as result of posting challenge
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-27 10:08:15 +02:00
8acb15a7fd outpost: fix flow executor not sending password for identification stage
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-27 09:43:07 +02:00
f601e04b38 web/flows: assign location from redirect challenge in request handler not render
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-27 09:43:00 +02:00
f50529cb5b build(deps): bump @docusaurus/preset-classic in /website (#1307)
Bumps [@docusaurus/preset-classic](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-preset-classic) from 2.0.0-beta.4 to 2.0.0-beta.5.
- [Release notes](https://github.com/facebook/docusaurus/releases)
- [Changelog](https://github.com/facebook/docusaurus/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/docusaurus/commits/v2.0.0-beta.5/packages/docusaurus-preset-classic)

---
updated-dependencies:
- dependency-name: "@docusaurus/preset-classic"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-27 09:20:09 +02:00
3f1b6f9ed4 build(deps): bump typescript from 4.3.5 to 4.4.2 in /web (#1306) 2021-08-27 08:36:43 +02:00
f1ab0f4314 build(deps): bump @patternfly/patternfly from 4.125.3 to 4.132.2 in /web (#1308) 2021-08-27 08:36:34 +02:00
4d1129f385 build(deps): bump boto3 from 1.18.29 to 1.18.30 (#1310) 2021-08-27 08:36:19 +02:00
03ac9c6e16 build(deps): bump @docusaurus/plugin-client-redirects in /website (#1309) 2021-08-27 08:36:11 +02:00
c0839924f1 build(deps): bump github.com/go-openapi/runtime from 0.19.30 to 0.19.31 (#1311) 2021-08-27 08:35:57 +02:00
91e3aa760a web: Update Web API Client version (#1305)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-08-26 19:06:13 +02:00
5c0681d57b website/docs: add 2021.8.2 docs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-26 18:56:42 +02:00
164 changed files with 3474 additions and 4623 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2021.8.2 current_version = 2021.8.5
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
@ -23,7 +23,7 @@ values =
[bumpversion:file:schema.yml] [bumpversion:file:schema.yml]
[bumpversion:file:.github/workflows/release.yml] [bumpversion:file:.github/workflows/release-publish.yml]
[bumpversion:file:authentik/__init__.py] [bumpversion:file:authentik/__init__.py]

302
.github/workflows/ci-main.yml vendored Normal file
View File

@ -0,0 +1,302 @@
name: authentik-ci-main
on:
push:
branches:
- master
- next
- version-*
paths-ignore:
- website
pull_request:
branches:
- master
env:
POSTGRES_DB: authentik
POSTGRES_USER: authentik
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
jobs:
lint-pylint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- id: cache-pipenv
uses: actions/cache@v2.1.6
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh
- name: run pylint
run: pipenv run pylint authentik tests lifecycle
lint-black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- id: cache-pipenv
uses: actions/cache@v2.1.6
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh
- name: run black
run: pipenv run black --check authentik tests lifecycle
lint-isort:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- id: cache-pipenv
uses: actions/cache@v2.1.6
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh
- name: run isort
run: pipenv run isort --check authentik tests lifecycle
lint-bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- id: cache-pipenv
uses: actions/cache@v2.1.6
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh
- name: run bandit
run: pipenv run bandit -r authentik tests lifecycle
lint-pyright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: prepare
run: |
scripts/ci_prepare.sh
npm install -g pyright@1.1.136
- name: run bandit
run: pipenv run pyright e2e lifecycle
test-migrations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- id: cache-pipenv
uses: actions/cache@v2.1.6
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh
- name: run migrations
run: pipenv run python -m lifecycle.migrate
test-migrations-from-stable:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: checkout stable
run: |
# Copy current, latest config to local
cp authentik/lib/default.yml local.env.yml
git checkout $(git describe --abbrev=0 --match 'version/*')
- id: cache-pipenv
uses: actions/cache@v2.1.6
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh
- name: run migrations to stable
run: pipenv run python -m lifecycle.migrate
- name: checkout current code
run: |
set -x
git checkout $GITHUB_REF
pipenv sync --dev
- name: migrate to latest
run: pipenv run python -m lifecycle.migrate
test-unittest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- id: cache-pipenv
uses: actions/cache@v2.1.6
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh
- uses: testspace-com/setup-testspace@v1
with:
domain: ${{github.repository_owner}}
- name: run unittest
run: |
pipenv run make test
pipenv run coverage xml
- name: run testspace
if: ${{ always() }}
run: |
testspace [unittest]unittest.xml --link=codecov
- if: ${{ always() }}
uses: codecov/codecov-action@v2
test-integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- id: cache-pipenv
uses: actions/cache@v2.1.6
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh
- uses: testspace-com/setup-testspace@v1
with:
domain: ${{github.repository_owner}}
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.2.0
- name: run integration
run: |
pipenv run make test-integration
pipenv run coverage xml
- name: run testspace
if: ${{ always() }}
run: |
testspace [integration]unittest.xml --link=codecov
- if: ${{ always() }}
uses: codecov/codecov-action@v2
test-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- uses: testspace-com/setup-testspace@v1
with:
domain: ${{github.repository_owner}}
- id: cache-pipenv
uses: actions/cache@v2.1.6
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: |
scripts/ci_prepare.sh
docker-compose -f tests/e2e/ci.docker-compose.yml up -d
- id: cache-web
uses: actions/cache@v2.1.6
with:
path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/**') }}
- name: prepare web ui
if: steps.cache-web.outputs.cache-hit != 'true'
run: |
cd web
npm i
npm run build
- name: run e2e
run: |
pipenv run make test-e2e
pipenv run coverage xml
- name: run testspace
if: ${{ always() }}
run: |
testspace [e2e]unittest.xml --link=codecov
- if: ${{ always() }}
uses: codecov/codecov-action@v2
build:
needs:
- lint-pylint
- lint-black
- lint-isort
- lint-bandit
- lint-pyright
- test-migrations
- test-migrations-from-stable
- test-unittest
- test-integration
- test-e2e
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: prepare variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
run: |
python ./scripts/gh_do_set_branch.py
- name: Login to Container Registry
uses: docker/login-action@v1
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
with:
registry: beryju.org
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_PASSWORD }}
- name: Building Docker Image
uses: docker/build-push-action@v2
with:
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
tags: |
beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }}
beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }}
build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}

75
.github/workflows/ci-outpost.yml vendored Normal file
View File

@ -0,0 +1,75 @@
name: authentik-ci-outpost
on:
push:
branches:
- master
- next
- version-*
pull_request:
branches:
- master
jobs:
lint-golint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '^1.16.3'
- name: Generate API
run: |
make gen-outpost
- name: Run linter
run: |
# Create folder structure for go embeds
mkdir -p web/dist
mkdir -p website/help
touch web/dist/test website/help/test
docker run \
--rm \
-v $(pwd):/app \
-w /app \
golangci/golangci-lint:v1.39.0 \
golangci-lint run -v --timeout 200s
build:
needs:
- lint-golint
strategy:
matrix:
type:
- proxy
- ldap
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: prepare variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
run: |
python ./scripts/gh_do_set_branch.py
- name: Login to Container Registry
uses: docker/login-action@v1
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
with:
registry: beryju.org
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_PASSWORD }}
- name: Building Docker Image
uses: docker/build-push-action@v2
with:
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
tags: |
beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }}
beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }}
beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.sha }}
file: ${{ matrix.type }}.Dockerfile
platforms: linux/amd64,linux/arm64
build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}

89
.github/workflows/ci-web.yml vendored Normal file
View File

@ -0,0 +1,89 @@
name: authentik-ci-web
on:
push:
branches:
- master
- next
- version-*
pull_request:
branches:
- master
jobs:
lint-eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- run: |
cd web
npm install
- name: Generate API
run: make gen-web
- name: Eslint
run: |
cd web
npm run lint
lint-prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- run: |
cd web
npm install
- name: Generate API
run: make gen-web
- name: prettier
run: |
cd web
npm run prettier-check
lint-lit-analyse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- run: |
cd web
npm install
- name: Generate API
run: make gen-web
- name: prettier
run: |
cd web
npm run lit-analyse
build:
needs:
- lint-eslint
- lint-prettier
- lint-lit-analyse
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- run: |
cd web
npm install
- name: Generate API
run: make gen-web
- name: build
run: |
cd web
npm run build

View File

@ -33,14 +33,14 @@ jobs:
with: with:
push: ${{ github.event_name == 'release' }} push: ${{ github.event_name == 'release' }}
tags: | tags: |
beryju/authentik:2021.8.2, beryju/authentik:2021.8.5,
beryju/authentik:latest, beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.8.2, ghcr.io/goauthentik/server:2021.8.5,
ghcr.io/goauthentik/server:latest ghcr.io/goauthentik/server:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
context: . context: .
- name: Building Docker Image (stable) - name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.8.2', 'rc') }} if: ${{ github.event_name == 'release' && !contains('2021.8.5', 'rc') }}
run: | run: |
docker pull beryju/authentik:latest docker pull beryju/authentik:latest
docker tag beryju/authentik:latest beryju/authentik:stable docker tag beryju/authentik:latest beryju/authentik:stable
@ -75,14 +75,14 @@ jobs:
with: with:
push: ${{ github.event_name == 'release' }} push: ${{ github.event_name == 'release' }}
tags: | tags: |
beryju/authentik-proxy:2021.8.2, beryju/authentik-proxy:2021.8.5,
beryju/authentik-proxy:latest, beryju/authentik-proxy:latest,
ghcr.io/goauthentik/proxy:2021.8.2, ghcr.io/goauthentik/proxy:2021.8.5,
ghcr.io/goauthentik/proxy:latest ghcr.io/goauthentik/proxy:latest
file: proxy.Dockerfile file: proxy.Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable) - name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.8.2', 'rc') }} if: ${{ github.event_name == 'release' && !contains('2021.8.5', 'rc') }}
run: | run: |
docker pull beryju/authentik-proxy:latest docker pull beryju/authentik-proxy:latest
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
@ -117,14 +117,14 @@ jobs:
with: with:
push: ${{ github.event_name == 'release' }} push: ${{ github.event_name == 'release' }}
tags: | tags: |
beryju/authentik-ldap:2021.8.2, beryju/authentik-ldap:2021.8.5,
beryju/authentik-ldap:latest, beryju/authentik-ldap:latest,
ghcr.io/goauthentik/ldap:2021.8.2, ghcr.io/goauthentik/ldap:2021.8.5,
ghcr.io/goauthentik/ldap:latest ghcr.io/goauthentik/ldap:latest
file: ldap.Dockerfile file: ldap.Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable) - name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.8.2', 'rc') }} if: ${{ github.event_name == 'release' && !contains('2021.8.5', 'rc') }}
run: | run: |
docker pull beryju/authentik-ldap:latest docker pull beryju/authentik-ldap:latest
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
@ -157,9 +157,9 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Setup Node.js environment - name: Setup Node.js environment
uses: actions/setup-node@v2.4.0 uses: actions/setup-node@v2
with: with:
node-version: 12.x node-version: '16'
- name: Build web api client and web ui - name: Build web api client and web ui
run: | run: |
export NODE_ENV=production export NODE_ENV=production
@ -175,7 +175,7 @@ jobs:
SENTRY_PROJECT: authentik SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org SENTRY_URL: https://sentry.beryju.org
with: with:
version: authentik@2021.8.2 version: authentik@2021.8.5
environment: beryjuorg-prod environment: beryjuorg-prod
sourcemaps: './web/dist' sourcemaps: './web/dist'
url_prefix: '~/static/dist' url_prefix: '~/static/dist'

View File

@ -12,7 +12,7 @@ jobs:
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '16.x' node-version: '16'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Generate API Client - name: Generate API Client
run: make gen-web run: make gen-web

View File

@ -98,5 +98,6 @@ COPY --from=builder /work/authentik /authentik-proxy
USER authentik USER authentik
ENV TMPDIR /dev/shm/ ENV TMPDIR /dev/shm/
ENV PYTHONUBUFFERED 1 ENV PYTHONUBUFFERED 1
ENV prometheus_multiproc_dir /dev/shm/
ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle" ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle"
ENTRYPOINT [ "/lifecycle/ak" ] ENTRYPOINT [ "/lifecycle/ak" ]

View File

@ -7,8 +7,6 @@ NPM_VERSION = $(shell python -m scripts.npm_version)
all: lint-fix lint test gen all: lint-fix lint test gen
test-integration: test-integration:
k3d cluster create || exit 0
k3d kubeconfig write -o ~/.kube/config --overwrite
coverage run manage.py test -v 3 tests/integration coverage run manage.py test -v 3 tests/integration
test-e2e: test-e2e:
@ -61,7 +59,7 @@ gen-outpost:
-i /local/schema.yml \ -i /local/schema.yml \
-g go \ -g go \
-o /local/api \ -o /local/api \
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true --additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true,disallowAdditionalPropertiesIfNotPresent=false
rm -f api/go.mod api/go.sum rm -f api/go.mod api/go.sum
gen: gen-build gen-clean gen-web gen-outpost gen: gen-build gen-clean gen-web gen-outpost

178
Pipfile.lock generated
View File

@ -122,19 +122,19 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:4dc7e346e92c01e8a997daa58a4c990151841d2d2962067325d963f665c7287a", "sha256:4df1085f5c24504a1b1a6584947f27b67c26eda123f29d3cecce9b2fd683e09b",
"sha256:79b7e6e0167def749352968ed6eb96954d9e2dd1dca8f297f122414753ce73a3" "sha256:a7fccb61d95230322dd812629455df14167307c569077fa89d297eae73605ffb"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.18.29" "version": "==1.18.36"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:1f16998b4f5a88e6844196feee7fa5eef6b36034d377f9845c7df12b8803b3be", "sha256:5b9a7d30e44b8a0a2bbbde62ae01bf6c349017e836985a0248552b00bbce7fae",
"sha256:fec924f63b40bd29b522fa109ecbc45f16eedcbeb22b68c6c79773c22a552b16" "sha256:e3e522fbe0bad1197aa7182451dc05f650310e77cf0a77749f6a5e82794c53de"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==1.21.29" "version": "==1.21.36"
}, },
"cachetools": { "cachetools": {
"hashes": [ "hashes": [
@ -359,11 +359,11 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:7f92413529aa0e291f3be78ab19be31aefb1e1c9a52cd59e130f505f27a51f13", "sha256:95b318319d6997bac3595517101ad9cc83fe5672ac498ba48d1a410f47afecd2",
"sha256:f27f8544c9d4c383bbe007c57e3235918e258364577373d4920e9162837be022" "sha256:e93c93565005b37ddebf2396b4dc4b6913c1838baa82efdfb79acedd5816c240"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.2.6" "version": "==3.2.7"
}, },
"django-dbbackup": { "django-dbbackup": {
"git": "https://github.com/django-dbbackup/django-dbbackup.git", "git": "https://github.com/django-dbbackup/django-dbbackup.git",
@ -443,19 +443,19 @@
}, },
"docker": { "docker": {
"hashes": [ "hashes": [
"sha256:3e8bc47534e0ca9331d72c32f2881bb13b93ded0bcdeab3c833fb7cf61c0a9a5", "sha256:21ec4998e90dff7a7aaaa098ca8d839c7de412b89e6f6c30908372d58fecf663",
"sha256:fc961d622160e8021c10d1bcabc388c57d55fb1f917175afbe24af442e6879bd" "sha256:9b17f0723d83c1f3418d2aa17bf90b24dbe97deda06208dd4262fa30a6ee87eb"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.0.0" "version": "==5.0.2"
}, },
"drf-spectacular": { "drf-spectacular": {
"hashes": [ "hashes": [
"sha256:5b1c27de127c86564be5a967a6fa195cfe161b552d98364282ae9e6ed3d75a85", "sha256:47ef6ec8ff48ac8aede6ec12450a55fee381cf84de969ef1724dcde5a93de6b8",
"sha256:8588706c27f44adfbb3405bae9ef9cd6506f4b59d4cbd66c59780dce035602d9" "sha256:d746b936cb4cddec380ea95bf91de6a6721777dfc42e0eea53b83c61a625e94e"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.18.0" "version": "==0.18.2"
}, },
"duo-client": { "duo-client": {
"hashes": [ "hashes": [
@ -490,11 +490,11 @@
}, },
"google-auth": { "google-auth": {
"hashes": [ "hashes": [
"sha256:c012c8be7c442c8309ca8fa0876fef33f5fd977c467be1e1c1c2f721e8ebd73c", "sha256:104475dc4d57bbae49017aea16fffbb763204fa2d6a70f1f3cc79962c1a383a4",
"sha256:ea1af050b3e06eb73e4470f704d23007307bc0e87c13e015f6b90460f1407bd3" "sha256:cde472372e030e1e0bc64dac00fb53e6c095d7ab641f4281e2c995e85e205d8b"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==2.0.1" "version": "==2.0.2"
}, },
"gunicorn": { "gunicorn": {
"hashes": [ "hashes": [
@ -1151,11 +1151,11 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e",
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7",
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"
], ],
"version": "==3.10.0.0" "version": "==3.10.0.2"
}, },
"ua-parser": { "ua-parser": {
"hashes": [ "hashes": [
@ -1294,20 +1294,20 @@
}, },
"xmlsec": { "xmlsec": {
"hashes": [ "hashes": [
"sha256:23f209260b37bdc2fd96af837494c47dd1e67964f077442b63acd83c0f62e212", "sha256:135724cdce60e6bbd072fca6f09a21f72e2cecc59eebb4eed7740c316ecabc7b",
"sha256:4fb38ab0bf3e47cbae136119674a869e09d61c939b510350f369c8ac46087373", "sha256:1b4377f6d37ad714ba95a227ef40fb54ba1b22ef5170ce04c330fe45ee6ad184",
"sha256:705ab5b848afdf3a5c78b1322276054c885f44dc51601e14cb883a9c86cbe20f", "sha256:2c86ac6ce570c9e04f04da0cd5e7d3db346e4b5b1d006311606368f17c756ef9",
"sha256:843d10bba4c480609da74ee11fff1ee0fc1c12821c656979f12a7a4ecb043e03", "sha256:4e5f565de311afa33aaee4724566e685f951afe301212b6cf82f98cf9d8a1749",
"sha256:86d54b93f8278e2f0c504d0744e39a483c1c7ce9993f2ca70184cc7770faa982", "sha256:9a2b8a780093b0fe8cecae53a81a8cd9edd50c08980d374c5317c91f065042d9",
"sha256:8922fba55a060ee81de4a7f5efc593c5bf121047763aecf0eead02e061c9d2db", "sha256:ce9c681adbc87b4f06c2b16725d9b2edbdbd508117dae4288b5faf78c1406038",
"sha256:c7b49d4fce83186b89f7ce6cec765245d36a70d0acc2f3ed0ba95c735b3667da", "sha256:d22da4d3dcc559fb2e54e782f39c9ddad5f8d5b356f86a79bbb80b0a45115c97",
"sha256:cd2eaaff7f31784a07dd99ce81fa767313df3ba1834faa4143ee2c07000cac7a", "sha256:db3e18ca883c01bbe28c9f5197c66f676c9772cf2d85f667e6122fc4d0702225",
"sha256:dea5bef9b5830c36ccb7a68a0d94d49eaea4d03fbbd04179652bf661b7e6e30f", "sha256:e4783f7814aa2a3e318385cce8ef87c82954b9a59535a48f67da4e2c21c08ce1",
"sha256:eadff662d89c80db409c69d82eb3e695e16d4a5e8ab56b5b22670a54e9c6ff20", "sha256:f32e54065f0404ceff71388daa7fa7df10e1fb800051dfe302d63abb0acf0020",
"sha256:ee233d0bc27fb8f447ca2622b0de2ac2df45b8795f02ef263825912011fe4fe9" "sha256:f5d242b1a19a36078608f5d7f4d561c5ca55cac8061a323a071c06275267dc19"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.3.11" "version": "==1.3.12"
}, },
"yarl": { "yarl": {
"hashes": [ "hashes": [
@ -1420,11 +1420,11 @@
}, },
"astroid": { "astroid": {
"hashes": [ "hashes": [
"sha256:b6c2d75cd7c2982d09e7d41d70213e863b3ba34d3bd4014e08f167cee966e99e", "sha256:3b680ce0419b8a771aba6190139a3998d14b413852506d99aff8dc2bf65ee67c",
"sha256:ecc50f9b3803ebf8ea19aa2c6df5622d8a5c31456a53c741d3be044d96ff0948" "sha256:dc1e8b28427d6bbef6b8842b18765ab58f558c42bb80540bd7648c98412af25e"
], ],
"markers": "python_version ~= '3.6'", "markers": "python_version ~= '3.6'",
"version": "==2.7.2" "version": "==2.7.3"
}, },
"attrs": { "attrs": {
"hashes": [ "hashes": [
@ -1652,19 +1652,19 @@
}, },
"platformdirs": { "platformdirs": {
"hashes": [ "hashes": [
"sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c", "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f",
"sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e" "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==2.2.0" "version": "==2.3.0"
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '3.6'",
"version": "==0.13.1" "version": "==1.0.0"
}, },
"py": { "py": {
"hashes": [ "hashes": [
@ -1707,11 +1707,11 @@
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
"sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.2.4" "version": "==6.2.5"
}, },
"pytest-django": { "pytest-django": {
"hashes": [ "hashes": [
@ -1758,49 +1758,49 @@
}, },
"regex": { "regex": {
"hashes": [ "hashes": [
"sha256:03840a07a402576b8e3a6261f17eb88abd653ad4e18ec46ef10c9a63f8c99ebd", "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468",
"sha256:06ba444bbf7ede3890a912bd4904bb65bf0da8f0d8808b90545481362c978642", "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354",
"sha256:1f9974826aeeda32a76648fc677e3125ade379869a84aa964b683984a2dea9f1", "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308",
"sha256:330836ad89ff0be756b58758878409f591d4737b6a8cef26a162e2a4961c3321", "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d",
"sha256:38600fd58c2996829480de7d034fb2d3a0307110e44dae80b6b4f9b3d2eea529", "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc",
"sha256:3a195e26df1fbb40ebee75865f9b64ba692a5824ecb91c078cc665b01f7a9a36", "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8",
"sha256:41acdd6d64cd56f857e271009966c2ffcbd07ec9149ca91f71088574eaa4278a", "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797",
"sha256:45f97ade892ace20252e5ccecdd7515c7df5feeb42c3d2a8b8c55920c3551c30", "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2",
"sha256:4b0c211c55d4aac4309c3209833c803fada3fc21cdf7b74abedda42a0c9dc3ce", "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13",
"sha256:5d5209c3ba25864b1a57461526ebde31483db295fc6195fdfc4f8355e10f7376", "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d",
"sha256:615fb5a524cffc91ab4490b69e10ae76c1ccbfa3383ea2fad72e54a85c7d47dd", "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a",
"sha256:61e734c2bcb3742c3f454dfa930ea60ea08f56fd1a0eb52d8cb189a2f6be9586", "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0",
"sha256:640ccca4d0a6fcc6590f005ecd7b16c3d8f5d52174e4854f96b16f34c39d6cb7", "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73",
"sha256:6dbd51c3db300ce9d3171f4106da18fe49e7045232630fe3d4c6e37cb2b39ab9", "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1",
"sha256:71a904da8c9c02aee581f4452a5a988c3003207cb8033db426f29e5b2c0b7aea", "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed",
"sha256:8021dee64899f993f4b5cca323aae65aabc01a546ed44356a0965e29d7893c94", "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a",
"sha256:8b8d551f1bd60b3e1c59ff55b9e8d74607a5308f66e2916948cafd13480b44a3", "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b",
"sha256:93f9f720081d97acee38a411e861d4ce84cbc8ea5319bc1f8e38c972c47af49f", "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f",
"sha256:96f0c79a70642dfdf7e6a018ebcbea7ea5205e27d8e019cad442d2acfc9af267", "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256",
"sha256:9966337353e436e6ba652814b0a957a517feb492a98b8f9d3b6ba76d22301dcc", "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb",
"sha256:a34ba9e39f8269fd66ab4f7a802794ffea6d6ac500568ec05b327a862c21ce23", "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2",
"sha256:a49f85f0a099a5755d0a2cc6fc337e3cb945ad6390ec892332c691ab0a045882", "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983",
"sha256:a795829dc522227265d72b25d6ee6f6d41eb2105c15912c230097c8f5bfdbcdc", "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb",
"sha256:a89ca4105f8099de349d139d1090bad387fe2b208b717b288699ca26f179acbe", "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645",
"sha256:ac95101736239260189f426b1e361dc1b704513963357dc474beb0f39f5b7759", "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8",
"sha256:ae87ab669431f611c56e581679db33b9a467f87d7bf197ac384e71e4956b4456", "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a",
"sha256:b091dcfee169ad8de21b61eb2c3a75f9f0f859f851f64fdaf9320759a3244239", "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906",
"sha256:b511c6009d50d5c0dd0bab85ed25bc8ad6b6f5611de3a63a59786207e82824bb", "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f",
"sha256:b79dc2b2e313565416c1e62807c7c25c67a6ff0a0f8d83a318df464555b65948", "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c",
"sha256:bca14dfcfd9aae06d7d8d7e105539bd77d39d06caaae57a1ce945670bae744e0", "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892",
"sha256:c835c30f3af5c63a80917b72115e1defb83de99c73bc727bddd979a3b449e183", "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0",
"sha256:ccd721f1d4fc42b541b633d6e339018a08dd0290dc67269df79552843a06ca92", "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e",
"sha256:d6c2b1d78ceceb6741d703508cd0e9197b34f6bf6864dab30f940f8886e04ade", "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e",
"sha256:d6ec4ae13760ceda023b2e5ef1f9bc0b21e4b0830458db143794a117fdbdc044", "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed",
"sha256:d8b623fc429a38a881ab2d9a56ef30e8ea20c72a891c193f5ebbddc016e083ee", "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c",
"sha256:ea9753d64cba6f226947c318a923dadaf1e21cd8db02f71652405263daa1f033", "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374",
"sha256:ebbceefbffae118ab954d3cd6bf718f5790db66152f95202ebc231d58ad4e2c2", "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd",
"sha256:ecb6e7c45f9cd199c10ec35262b53b2247fb9a408803ed00ee5bb2b54aa626f5", "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791",
"sha256:ef9326c64349e2d718373415814e754183057ebc092261387a2c2f732d9172b2", "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a",
"sha256:f93a9d8804f4cec9da6c26c8cfae2c777028b4fdd9f49de0302e26e00bb86504", "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1",
"sha256:faf08b0341828f6a29b8f7dd94d5cf8cc7c39bfc3e67b78514c54b494b66915a" "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"
], ],
"version": "==2021.8.21" "version": "==2021.8.28"
}, },
"requests": { "requests": {
"hashes": [ "hashes": [

View File

@ -4,14 +4,15 @@
--- ---
[![](https://img.shields.io/discord/809154715984199690?label=Discord&style=for-the-badge)](https://discord.gg/jg33eMhnj6) [![Join Discord](https://img.shields.io/discord/809154715984199690?label=Discord&style=for-the-badge)](https://discord.gg/jg33eMhnj6)
[![CI Build status](https://img.shields.io/azure-devops/build/beryjuorg/authentik/6?style=for-the-badge)](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=6) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/goauthentik/authentik/authentik-ci-main?label=core%20build&style=for-the-badge)](https://github.com/goauthentik/authentik/actions/workflows/ci-main.yml)
[![Tests](https://img.shields.io/azure-devops/tests/beryjuorg/authentik/6?compact_message&style=for-the-badge)](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=6) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/goauthentik/authentik/authentik-ci-outpost?label=outpost%20build&style=for-the-badge)](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml)
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/goauthentik/authentik/authentik-ci-web?label=web%20build&style=for-the-badge)](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
[![Code Coverage](https://img.shields.io/codecov/c/gh/goauthentik/authentik?style=for-the-badge)](https://codecov.io/gh/goauthentik/authentik) [![Code Coverage](https://img.shields.io/codecov/c/gh/goauthentik/authentik?style=for-the-badge)](https://codecov.io/gh/goauthentik/authentik)
[![Testspace tests](https://img.shields.io/testspace/total/goauthentik/goauthentik:authentik/master?style=for-the-badge)](https://goauthentik.testspace.com/)
![Docker pulls](https://img.shields.io/docker/pulls/beryju/authentik.svg?style=for-the-badge) ![Docker pulls](https://img.shields.io/docker/pulls/beryju/authentik.svg?style=for-the-badge)
![Latest version](https://img.shields.io/docker/v/beryju/authentik?sort=semver&style=for-the-badge) ![Latest version](https://img.shields.io/docker/v/beryju/authentik?sort=semver&style=for-the-badge)
![LGTM Grade](https://img.shields.io/lgtm/grade/python/github/goauthentik/authentik?style=for-the-badge) [![](https://img.shields.io/badge/Help%20translate-transifex-blue?style=for-the-badge)](https://www.transifex.com/beryjuorg/authentik/)
[Transifex](https://www.transifex.com/beryjuorg/authentik/)
## What is authentik? ## What is authentik?

View File

@ -6,9 +6,8 @@
| Version | Supported | | Version | Supported |
| ---------- | ------------------ | | ---------- | ------------------ |
| 2021.5.x | :white_check_mark: |
| 2021.6.x | :white_check_mark: |
| 2021.7.x | :white_check_mark: | | 2021.7.x | :white_check_mark: |
| 2021.8.x | :white_check_mark: |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@ -1,3 +1,3 @@
"""authentik""" """authentik"""
__version__ = "2021.8.2" __version__ = "2021.8.5"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -1,8 +1,10 @@
"""authentik api urls""" """authentik api urls"""
from django.urls import include, path from django.urls import include, path
from authentik.api.v2.urls import urlpatterns as v2_urls from authentik.api.v3.urls import urlpatterns as v3_urls
urlpatterns = [ urlpatterns = [
path("v2beta/", include(v2_urls)), # Remove in 2022.1
path("v2beta/", include(v3_urls)),
path("v3/", include(v3_urls)),
] ]

View File

@ -4,16 +4,44 @@ from json import loads
from django.conf import settings from django.conf import settings
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.views.generic.base import View
from requests import post from requests import post
from requests.exceptions import RequestException from requests.exceptions import RequestException
from rest_framework.authentication import SessionAuthentication
from rest_framework.parsers import BaseParser
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.throttling import AnonRateThrottle
from rest_framework.views import APIView
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
class SentryTunnelView(View): class PlainTextParser(BaseParser):
"""Plain text parser."""
media_type = "text/plain"
def parse(self, stream, media_type=None, parser_context=None) -> str:
"""Simply return a string representing the body of the request."""
return stream.read()
class CsrfExemptSessionAuthentication(SessionAuthentication):
"""CSRF-exempt Session authentication"""
def enforce_csrf(self, request: Request):
return # To not perform the csrf check previously happening
class SentryTunnelView(APIView):
"""Sentry tunnel, to prevent ad blockers from blocking sentry""" """Sentry tunnel, to prevent ad blockers from blocking sentry"""
serializer_class = None
parser_classes = [PlainTextParser]
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
authentication_classes = [CsrfExemptSessionAuthentication]
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Sentry tunnel, to prevent ad blockers from blocking sentry""" """Sentry tunnel, to prevent ad blockers from blocking sentry"""
# Only allow usage of this endpoint when error reporting is enabled # Only allow usage of this endpoint when error reporting is enabled

View File

@ -1,6 +1,6 @@
"""api v2 urls""" """api v3 urls"""
from django.urls import path from django.urls import path
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.cache import cache_page
from drf_spectacular.views import SpectacularAPIView from drf_spectacular.views import SpectacularAPIView
from rest_framework import routers from rest_framework import routers
@ -10,8 +10,8 @@ from authentik.admin.api.system import SystemView
from authentik.admin.api.tasks import TaskViewSet from authentik.admin.api.tasks import TaskViewSet
from authentik.admin.api.version import VersionView from authentik.admin.api.version import VersionView
from authentik.admin.api.workers import WorkerView from authentik.admin.api.workers import WorkerView
from authentik.api.v2.config import ConfigView from authentik.api.v3.config import ConfigView
from authentik.api.v2.sentry import SentryTunnelView from authentik.api.v3.sentry import SentryTunnelView
from authentik.api.views import APIBrowserView from authentik.api.views import APIBrowserView
from authentik.core.api.applications import ApplicationViewSet from authentik.core.api.applications import ApplicationViewSet
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
@ -225,7 +225,7 @@ urlpatterns = (
FlowExecutorView.as_view(), FlowExecutorView.as_view(),
name="flow-executor", name="flow-executor",
), ),
path("sentry/", csrf_exempt(SentryTunnelView.as_view()), name="sentry"), path("sentry/", SentryTunnelView.as_view(), name="sentry"),
path("schema/", SpectacularAPIView.as_view(), name="schema"), path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"),
] ]
) )

View File

@ -67,7 +67,7 @@ class ApplicationSerializer(ModelSerializer):
class ApplicationViewSet(UsedByMixin, ModelViewSet): class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Application Viewset""" """Application Viewset"""
queryset = Application.objects.all() queryset = Application.objects.all().prefetch_related("provider")
serializer_class = ApplicationSerializer serializer_class = ApplicationSerializer
search_fields = [ search_fields = [
"name", "name",

View File

@ -81,7 +81,7 @@ class GroupFilter(FilterSet):
class GroupViewSet(UsedByMixin, ModelViewSet): class GroupViewSet(UsedByMixin, ModelViewSet):
"""Group Viewset""" """Group Viewset"""
queryset = Group.objects.all() queryset = Group.objects.all().select_related("parent").prefetch_related("users")
serializer_class = GroupSerializer serializer_class = GroupSerializer
search_fields = ["name", "is_superuser"] search_fields = ["name", "is_superuser"]
filterset_class = GroupFilter filterset_class = GroupFilter

View File

@ -23,7 +23,7 @@ from authentik.managed.api import ManagedSerializer
class TokenSerializer(ManagedSerializer, ModelSerializer): class TokenSerializer(ManagedSerializer, ModelSerializer):
"""Token Serializer""" """Token Serializer"""
user_obj = UserSerializer(required=False) user_obj = UserSerializer(required=False, source="user")
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created.""" """Ensure only API or App password tokens are created."""

View File

@ -1,8 +1,13 @@
"""Notification API Views""" """Notification API Views"""
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField from rest_framework.fields import ReadOnlyField
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
@ -53,3 +58,18 @@ class NotificationViewSet(
] ]
permission_classes = [OwnerPermissions] permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
@extend_schema(
request=OpenApiTypes.NONE,
responses={
204: OpenApiResponse(description="Marked tasks as read successfully."),
},
)
@action(detail=False, methods=["post"])
def mark_all_seen(self, request: Request) -> Response:
"""Mark all the user's notifications as seen"""
notifications = Notification.objects.filter(user=request.user)
for notification in notifications:
notification.seen = True
Notification.objects.bulk_update(notifications, ["seen"])
return Response({}, status=204)

View File

@ -1,10 +1,7 @@
"""authentik events app""" """authentik events app"""
from datetime import timedelta
from importlib import import_module from importlib import import_module
from django.apps import AppConfig from django.apps import AppConfig
from django.db import ProgrammingError
from django.utils.timezone import now
class AuthentikEventsConfig(AppConfig): class AuthentikEventsConfig(AppConfig):
@ -16,12 +13,3 @@ class AuthentikEventsConfig(AppConfig):
def ready(self): def ready(self):
import_module("authentik.events.signals") import_module("authentik.events.signals")
try:
from authentik.events.models import Event
date_from = now() - timedelta(days=1)
for event in Event.objects.filter(created__gte=date_from):
event._set_prom_metrics()
except ProgrammingError:
pass

View File

@ -10,7 +10,6 @@ from django.db import models
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from prometheus_client import Gauge
from requests import RequestException, post from requests import RequestException, post
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -28,11 +27,6 @@ from authentik.tenants.models import Tenant
from authentik.tenants.utils import DEFAULT_TENANT from authentik.tenants.utils import DEFAULT_TENANT
LOGGER = get_logger("authentik.events") LOGGER = get_logger("authentik.events")
GAUGE_EVENTS = Gauge(
"authentik_events",
"Events in authentik",
["action", "user_username", "app", "client_ip"],
)
def default_event_duration(): def default_event_duration():
@ -182,14 +176,6 @@ class Event(ExpiringModel):
return return
self.context["geo"] = city self.context["geo"] = city
def _set_prom_metrics(self):
GAUGE_EVENTS.labels(
action=self.action,
user_username=self.user.get("username"),
app=self.app,
client_ip=self.client_ip,
).set(self.created.timestamp())
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self._state.adding: if self._state.adding:
LOGGER.debug( LOGGER.debug(
@ -200,7 +186,6 @@ class Event(ExpiringModel):
user=self.user, user=self.user,
) )
super().save(*args, **kwargs) super().save(*args, **kwargs)
self._set_prom_metrics()
@property @property
def summary(self) -> str: def summary(self) -> str:

View File

@ -11,6 +11,7 @@ from django.core.cache import cache
from prometheus_client import Gauge from prometheus_client import Gauge
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.lib.utils.errors import exception_to_string
GAUGE_TASKS = Gauge( GAUGE_TASKS = Gauge(
"authentik_system_tasks", "authentik_system_tasks",
@ -174,9 +175,7 @@ class MonitoredTask(Task):
).save(self.result_timeout_hours) ).save(self.result_timeout_hours)
Event.new( Event.new(
EventAction.SYSTEM_TASK_EXCEPTION, EventAction.SYSTEM_TASK_EXCEPTION,
message=( message=(f"Task {self.__name__} encountered an error: {exception_to_string(exc)}"),
f"Task {self.__name__} encountered an error: " "\n".join(self._result.messages)
),
).save() ).save()
return super().on_failure(exc, task_id, args, kwargs, einfo=einfo) return super().on_failure(exc, task_id, args, kwargs, einfo=einfo)

View File

@ -4,15 +4,15 @@ from django.urls import reverse
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction, Notification, NotificationSeverity
class TestEventsAPI(APITestCase): class TestEventsAPI(APITestCase):
"""Test Event API""" """Test Event API"""
def setUp(self) -> None: def setUp(self) -> None:
user = User.objects.get(username="akadmin") self.user = User.objects.get(username="akadmin")
self.client.force_login(user) self.client.force_login(self.user)
def test_top_n(self): def test_top_n(self):
"""Test top_per_user""" """Test top_per_user"""
@ -30,3 +30,14 @@ class TestEventsAPI(APITestCase):
reverse("authentik_api:event-actions"), reverse("authentik_api:event-actions"),
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_notifications(self):
"""Test notifications"""
notification = Notification.objects.create(
user=self.user, severity=NotificationSeverity.ALERT, body="", seen=False
)
self.client.post(
reverse("authentik_api:notification-mark-all-seen"),
)
notification.refresh_from_db()
self.assertTrue(notification.seen)

View File

@ -31,6 +31,7 @@ class FlowPlanProcess(PROCESS_CLASS): # pragma: no cover
self.request = RequestFactory().get("/") self.request = RequestFactory().get("/")
def run(self): def run(self):
"""Execute 1000 flow plans"""
print(f"Proc {self.index} Running") print(f"Proc {self.index} Running")
def test_inner(): def test_inner():

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.6 on 2021-08-30 14:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0023_alter_flow_background"),
]
operations = [
migrations.AlterField(
model_name="flow",
name="compatibility_mode",
field=models.BooleanField(
default=False,
help_text="Enable compatibility mode, increases compatibility with password managers on mobile devices.",
),
),
]

View File

@ -125,7 +125,7 @@ class Flow(SerializerModel, PolicyBindingModel):
) )
compatibility_mode = models.BooleanField( compatibility_mode = models.BooleanField(
default=True, default=False,
help_text=_( help_text=_(
"Enable compatibility mode, increases compatibility with " "Enable compatibility mode, increases compatibility with "
"password managers on mobile devices." "password managers on mobile devices."

View File

@ -2,10 +2,10 @@
from unittest.mock import MagicMock, PropertyMock, patch from unittest.mock import MagicMock, PropertyMock, patch
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
@ -37,7 +37,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse):
TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response) TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response)
class TestFlowExecutor(TestCase): class TestFlowExecutor(APITestCase):
"""Test views logic""" """Test views logic"""
def setUp(self): def setUp(self):

View File

@ -1,5 +1,5 @@
"""flow views tests""" """flow views tests"""
from django.test import Client, TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from authentik.flows.models import Flow, FlowDesignation from authentik.flows.models import Flow, FlowDesignation
@ -10,9 +10,6 @@ from authentik.flows.views import SESSION_KEY_PLAN
class TestHelperView(TestCase): class TestHelperView(TestCase):
"""Test helper views logic""" """Test helper views logic"""
def setUp(self):
self.client = Client()
def test_default_view(self): def test_default_view(self):
"""Test that ToDefaultFlow returns the expected URL""" """Test that ToDefaultFlow returns the expected URL"""
flow = Flow.objects.filter( flow = Flow.objects.filter(

View File

@ -15,7 +15,7 @@ from authentik.core.channels import AuthJsonConsumer
from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState
GAUGE_OUTPOSTS_CONNECTED = Gauge( GAUGE_OUTPOSTS_CONNECTED = Gauge(
"authentik_outposts_connected", "Currently connected outposts", ["outpost", "uid"] "authentik_outposts_connected", "Currently connected outposts", ["outpost", "uid", "expected"]
) )
GAUGE_OUTPOSTS_LAST_UPDATE = Gauge( GAUGE_OUTPOSTS_LAST_UPDATE = Gauge(
"authentik_outposts_last_update", "authentik_outposts_last_update",
@ -76,6 +76,7 @@ class OutpostConsumer(AuthJsonConsumer):
GAUGE_OUTPOSTS_CONNECTED.labels( GAUGE_OUTPOSTS_CONNECTED.labels(
outpost=self.outpost.name, outpost=self.outpost.name,
uid=self.last_uid, uid=self.last_uid,
expected=self.outpost.config.kubernetes_replicas,
).dec() ).dec()
LOGGER.debug( LOGGER.debug(
"removed outpost instance from cache", "removed outpost instance from cache",
@ -100,6 +101,7 @@ class OutpostConsumer(AuthJsonConsumer):
GAUGE_OUTPOSTS_CONNECTED.labels( GAUGE_OUTPOSTS_CONNECTED.labels(
outpost=self.outpost.name, outpost=self.outpost.name,
uid=self.last_uid, uid=self.last_uid,
expected=self.outpost.config.kubernetes_replicas,
).inc() ).inc()
LOGGER.debug( LOGGER.debug(
"added outpost instace to cache", "added outpost instace to cache",

View File

@ -29,7 +29,9 @@ class DockerController(BaseController):
raise ControllerException from exc raise ControllerException from exc
def _get_labels(self) -> dict[str, str]: def _get_labels(self) -> dict[str, str]:
return {} return {
"io.goauthentik.outpost-uuid": self.outpost.pk.hex,
}
def _get_env(self) -> dict[str, str]: def _get_env(self) -> dict[str, str]:
return { return {
@ -49,6 +51,17 @@ class DockerController(BaseController):
return True return True
return False return False
def _comp_labels(self, container: Container) -> bool:
"""Check if container's labels is equal to what we would set. Return true if container needs
to be rebuilt."""
should_be = self._get_labels()
for key, expected_value in should_be.items():
if key not in container.labels:
return True
if container.labels[key] != expected_value:
return True
return False
def _comp_ports(self, container: Container) -> bool: def _comp_ports(self, container: Container) -> bool:
"""Check that the container has the correct ports exposed. Return true if container needs """Check that the container has the correct ports exposed. Return true if container needs
to be rebuilt.""" to be rebuilt."""
@ -92,9 +105,11 @@ class DockerController(BaseController):
"environment": self._get_env(), "environment": self._get_env(),
"labels": self._get_labels(), "labels": self._get_labels(),
"restart_policy": {"Name": "unless-stopped"}, "restart_policy": {"Name": "unless-stopped"},
"network": self.outpost.config.docker_network,
} }
if settings.TEST: if settings.TEST:
del container_args["ports"] del container_args["ports"]
del container_args["network"]
container_args["network_mode"] = "host" container_args["network_mode"] = "host"
return ( return (
self.client.containers.create(**container_args), self.client.containers.create(**container_args),
@ -133,6 +148,11 @@ class DockerController(BaseController):
self.logger.info("Container has outdated config, re-creating...") self.logger.info("Container has outdated config, re-creating...")
self.down() self.down()
return self.up(depth + 1) return self.up(depth + 1)
# Check that container values match our values
if self._comp_labels(container):
self.logger.info("Container has outdated labels, re-creating...")
self.down()
return self.up(depth + 1)
if ( if (
container.attrs.get("HostConfig", {}) container.attrs.get("HostConfig", {})
.get("RestartPolicy", {}) .get("RestartPolicy", {})

View File

@ -3,10 +3,11 @@ from typing import TYPE_CHECKING, Generic, TypeVar
from django.utils.text import slugify from django.utils.text import slugify
from kubernetes.client import V1ObjectMeta from kubernetes.client import V1ObjectMeta
from kubernetes.client.exceptions import ApiException, OpenApiException
from kubernetes.client.models.v1_deployment import V1Deployment from kubernetes.client.models.v1_deployment import V1Deployment
from kubernetes.client.models.v1_pod import V1Pod from kubernetes.client.models.v1_pod import V1Pod
from kubernetes.client.rest import ApiException
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from urllib3.exceptions import HTTPError
from authentik import __version__ from authentik import __version__
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
@ -72,8 +73,9 @@ class KubernetesObjectReconciler(Generic[T]):
try: try:
try: try:
current = self.retrieve() current = self.retrieve()
except ApiException as exc: except (OpenApiException, HTTPError) as exc:
if exc.status == 404: # pylint: disable=no-member
if isinstance(exc, ApiException) and exc.status == 404:
self.logger.debug("Failed to get current, triggering recreate") self.logger.debug("Failed to get current, triggering recreate")
raise NeedsRecreate from exc raise NeedsRecreate from exc
self.logger.debug("Other unhandled error", exc=exc) self.logger.debug("Other unhandled error", exc=exc)
@ -104,8 +106,9 @@ class KubernetesObjectReconciler(Generic[T]):
current = self.retrieve() current = self.retrieve()
self.delete(current) self.delete(current)
self.logger.debug("Removing") self.logger.debug("Removing")
except ApiException as exc: except (OpenApiException, HTTPError) as exc:
if exc.status == 404: # pylint: disable=no-member
if isinstance(exc, ApiException) and exc.status == 404:
self.logger.debug("Failed to get current, assuming non-existant") self.logger.debug("Failed to get current, assuming non-existant")
return return
self.logger.debug("Other unhandled error", exc=exc) self.logger.debug("Other unhandled error", exc=exc)

View File

@ -3,8 +3,9 @@ from io import StringIO
from typing import Type from typing import Type
from kubernetes.client.api_client import ApiClient from kubernetes.client.api_client import ApiClient
from kubernetes.client.exceptions import ApiException from kubernetes.client.exceptions import OpenApiException
from structlog.testing import capture_logs from structlog.testing import capture_logs
from urllib3.exceptions import HTTPError
from yaml import dump_all from yaml import dump_all
from authentik.outposts.controllers.base import BaseController, ControllerException from authentik.outposts.controllers.base import BaseController, ControllerException
@ -12,7 +13,7 @@ from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
from authentik.outposts.controllers.k8s.secret import SecretReconciler from authentik.outposts.controllers.k8s.secret import SecretReconciler
from authentik.outposts.controllers.k8s.service import ServiceReconciler from authentik.outposts.controllers.k8s.service import ServiceReconciler
from authentik.outposts.models import KubernetesServiceConnection, Outpost from authentik.outposts.models import KubernetesServiceConnection, Outpost, ServiceConnectionInvalid
class KubernetesController(BaseController): class KubernetesController(BaseController):
@ -40,7 +41,7 @@ class KubernetesController(BaseController):
reconciler = self.reconcilers[reconcile_key](self) reconciler = self.reconcilers[reconcile_key](self)
reconciler.up() reconciler.up()
except ApiException as exc: except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
raise ControllerException(str(exc)) from exc raise ControllerException(str(exc)) from exc
def up_with_logs(self) -> list[str]: def up_with_logs(self) -> list[str]:
@ -55,7 +56,7 @@ class KubernetesController(BaseController):
reconciler.up() reconciler.up()
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs] all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
return all_logs return all_logs
except ApiException as exc: except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
raise ControllerException(str(exc)) from exc raise ControllerException(str(exc)) from exc
def down(self): def down(self):
@ -65,7 +66,7 @@ class KubernetesController(BaseController):
self.logger.debug("Tearing down object", name=reconcile_key) self.logger.debug("Tearing down object", name=reconcile_key)
reconciler.down() reconciler.down()
except ApiException as exc: except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
raise ControllerException(str(exc)) from exc raise ControllerException(str(exc)) from exc
def down_with_logs(self) -> list[str]: def down_with_logs(self) -> list[str]:
@ -80,7 +81,7 @@ class KubernetesController(BaseController):
reconciler.down() reconciler.down()
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs] all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
return all_logs return all_logs
except ApiException as exc: except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
raise ControllerException(str(exc)) from exc raise ControllerException(str(exc)) from exc
def get_static_deployment(self) -> str: def get_static_deployment(self) -> str:

View File

@ -56,6 +56,7 @@ class ServiceConnectionInvalid(SentryIgnoredException):
@dataclass @dataclass
# pylint: disable=too-many-instance-attributes
class OutpostConfig: class OutpostConfig:
"""Configuration an outpost uses to configure it self""" """Configuration an outpost uses to configure it self"""
@ -67,8 +68,10 @@ class OutpostConfig:
log_level: str = CONFIG.y("log_level") log_level: str = CONFIG.y("log_level")
error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled") error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled")
error_reporting_environment: str = CONFIG.y("error_reporting.environment", "customer") error_reporting_environment: str = CONFIG.y("error_reporting.environment", "customer")
object_naming_template: str = field(default="ak-outpost-%(name)s") object_naming_template: str = field(default="ak-outpost-%(name)s")
docker_network: Optional[str] = field(default=None)
kubernetes_replicas: int = field(default=1) kubernetes_replicas: int = field(default=1)
kubernetes_namespace: str = field(default_factory=get_namespace) kubernetes_namespace: str = field(default_factory=get_namespace)
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict) kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
@ -362,7 +365,7 @@ class Outpost(ManagedModel):
) )
try: try:
assign_perm(code_name, user, model_or_perm) assign_perm(code_name, user, model_or_perm)
except Permission.DoesNotExist as exc: except (Permission.DoesNotExist, AttributeError) as exc:
LOGGER.warning( LOGGER.warning(
"permission doesn't exist", "permission doesn't exist",
code_name=code_name, code_name=code_name,

View File

@ -8,8 +8,11 @@ from structlog.stdlib import get_logger
from authentik.policies.models import Policy from authentik.policies.models import Policy
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
LOGGER = get_logger() LOGGER = get_logger()
RE_LOWER = re.compile("[a-z]")
RE_UPPER = re.compile("[A-Z]")
class PasswordPolicy(Policy): class PasswordPolicy(Policy):
@ -38,31 +41,42 @@ class PasswordPolicy(Policy):
return "ak-policy-password-form" return "ak-policy-password-form"
def passes(self, request: PolicyRequest) -> PolicyResult: def passes(self, request: PolicyRequest) -> PolicyResult:
if self.password_field not in request.context: if (
self.password_field not in request.context
and self.password_field not in request.context.get(PLAN_CONTEXT_PROMPT, {})
):
LOGGER.warning( LOGGER.warning(
"Password field not set in Policy Request", "Password field not set in Policy Request",
field=self.password_field, field=self.password_field,
fields=request.context.keys(), fields=request.context.keys(),
prompt_fields=request.context.get(PLAN_CONTEXT_PROMPT, {}).keys(),
) )
return PolicyResult(False, _("Password not set in context")) return PolicyResult(False, _("Password not set in context"))
password = request.context[self.password_field]
filter_regex = [] if self.password_field in request.context:
if self.amount_lowercase > 0: password = request.context[self.password_field]
filter_regex.append(r"[a-z]{%d,}" % self.amount_lowercase) else:
if self.amount_uppercase > 0: password = request.context[PLAN_CONTEXT_PROMPT][self.password_field]
filter_regex.append(r"[A-Z]{%d,}" % self.amount_uppercase)
if len(password) < self.length_min:
LOGGER.debug("password failed", reason="length")
return PolicyResult(False, self.error_message)
if self.amount_lowercase > 0 and len(RE_LOWER.findall(password)) < self.amount_lowercase:
LOGGER.debug("password failed", reason="amount_lowercase")
return PolicyResult(False, self.error_message)
if self.amount_uppercase > 0 and len(RE_UPPER.findall(password)) < self.amount_lowercase:
LOGGER.debug("password failed", reason="amount_uppercase")
return PolicyResult(False, self.error_message)
if self.amount_symbols > 0: if self.amount_symbols > 0:
filter_regex.append(r"[%s]{%d,}" % (self.symbol_charset, self.amount_symbols)) count = 0
full_regex = "|".join(filter_regex) for symbol in self.symbol_charset:
LOGGER.debug("Built regex", regexp=full_regex) count += password.count(symbol)
result = bool(re.compile(full_regex).match(password)) if count < self.amount_symbols:
LOGGER.debug("password failed", reason="amount_symbols")
return PolicyResult(False, self.error_message)
result = result and len(password) >= self.length_min return PolicyResult(True)
if not result:
return PolicyResult(result, self.error_message)
return PolicyResult(result)
class Meta: class Meta:

View File

@ -1,57 +0,0 @@
"""Password Policy tests"""
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.policies.password.models import PasswordPolicy
from authentik.policies.types import PolicyRequest, PolicyResult
class TestPasswordPolicy(TestCase):
"""Test Password Policy"""
def test_invalid(self):
"""Test without password"""
policy = PasswordPolicy.objects.create(
name="test_invalid",
amount_uppercase=1,
amount_lowercase=2,
amount_symbols=3,
length_min=24,
error_message="test message",
)
request = PolicyRequest(get_anonymous_user())
result: PolicyResult = policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages[0], "Password not set in context")
def test_false(self):
"""Failing password case"""
policy = PasswordPolicy.objects.create(
name="test_false",
amount_uppercase=1,
amount_lowercase=2,
amount_symbols=3,
length_min=24,
error_message="test message",
)
request = PolicyRequest(get_anonymous_user())
request.context["password"] = "test"
result: PolicyResult = policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages, ("test message",))
def test_true(self):
"""Positive password case"""
policy = PasswordPolicy.objects.create(
name="test_true",
amount_uppercase=1,
amount_lowercase=2,
amount_symbols=3,
length_min=3,
error_message="test message",
)
request = PolicyRequest(get_anonymous_user())
request.context["password"] = "Test()!"
result: PolicyResult = policy.passes(request)
self.assertTrue(result.passing)
self.assertEqual(result.messages, tuple())

View File

@ -0,0 +1,80 @@
"""Password flow tests"""
from django.urls.base import reverse
from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.policies.password.models import PasswordPolicy
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
class TestPasswordPolicyFlow(APITestCase):
"""Test Password Policy"""
def setUp(self) -> None:
self.user = User.objects.create(username="unittest", email="test@beryju.org")
self.flow = Flow.objects.create(
name="test-prompt",
slug="test-prompt",
designation=FlowDesignation.AUTHENTICATION,
)
password_prompt = Prompt.objects.create(
field_key="password",
label="PASSWORD_LABEL",
type=FieldTypes.PASSWORD,
required=True,
placeholder="PASSWORD_PLACEHOLDER",
)
self.policy = PasswordPolicy.objects.create(
name="test_true",
amount_uppercase=1,
amount_lowercase=2,
amount_symbols=3,
length_min=3,
error_message="test message",
)
stage = PromptStage.objects.create(name="prompt-stage")
stage.validation_policies.set([self.policy])
stage.fields.set(
[
password_prompt,
]
)
FlowStageBinding.objects.create(target=self.flow, stage=stage, order=2)
def test_prompt_data(self):
"""Test policy attached to a prompt stage"""
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
{"password": "akadmin"},
)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{
"component": "ak-stage-prompt",
"fields": [
{
"field_key": "password",
"label": "PASSWORD_LABEL",
"order": 0,
"placeholder": "PASSWORD_PLACEHOLDER",
"required": True,
"type": "password",
}
],
"flow_info": {
"background": self.flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
"title": "",
},
"response_errors": {
"non_field_errors": [{"code": "invalid", "string": self.policy.error_message}]
},
"type": ChallengeTypes.NATIVE.value,
},
)

View File

@ -0,0 +1,68 @@
"""Password Policy tests"""
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.lib.generators import generate_key
from authentik.policies.password.models import PasswordPolicy
from authentik.policies.types import PolicyRequest, PolicyResult
class TestPasswordPolicy(TestCase):
"""Test Password Policy"""
def setUp(self) -> None:
self.policy = PasswordPolicy.objects.create(
name="test_false",
amount_uppercase=1,
amount_lowercase=2,
amount_symbols=3,
length_min=24,
error_message="test message",
)
def test_invalid(self):
"""Test without password"""
request = PolicyRequest(get_anonymous_user())
result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages[0], "Password not set in context")
def test_failed_length(self):
"""Password too short"""
request = PolicyRequest(get_anonymous_user())
request.context["password"] = "test"
result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages, ("test message",))
def test_failed_lowercase(self):
"""not enough lowercase"""
request = PolicyRequest(get_anonymous_user())
request.context["password"] = "TTTTTTTTTTTTTTTTTTTTTTTe"
result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages, ("test message",))
def test_failed_uppercase(self):
"""not enough uppercase"""
request = PolicyRequest(get_anonymous_user())
request.context["password"] = "tttttttttttttttttttttttE"
result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages, ("test message",))
def test_failed_symbols(self):
"""not enough uppercase"""
request = PolicyRequest(get_anonymous_user())
request.context["password"] = "TETETETETETETETETETETETETe!!!"
result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages, ("test message",))
def test_true(self):
"""Positive password case"""
request = PolicyRequest(get_anonymous_user())
request.context["password"] = generate_key() + "ee!!!"
result: PolicyResult = self.policy.passes(request)
self.assertTrue(result.passing)
self.assertEqual(result.messages, tuple())

View File

@ -29,7 +29,19 @@ class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
queryset = LDAPProvider.objects.all() queryset = LDAPProvider.objects.all()
serializer_class = LDAPProviderSerializer serializer_class = LDAPProviderSerializer
filterset_fields = "__all__" filterset_fields = {
"application": ["isnull"],
"name": ["iexact"],
"authorization_flow__slug": ["iexact"],
"base_dn": ["iexact"],
"search_group__group_uuid": ["iexact"],
"search_group__name": ["iexact"],
"certificate__kp_uuid": ["iexact"],
"certificate__name": ["iexact"],
"tls_server_name": ["iexact"],
"uid_start_number": ["iexact"],
"gid_start_number": ["iexact"],
}
ordering = ["name"] ordering = ["name"]

View File

@ -80,7 +80,24 @@ class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
queryset = ProxyProvider.objects.all() queryset = ProxyProvider.objects.all()
serializer_class = ProxyProviderSerializer serializer_class = ProxyProviderSerializer
filterset_fields = "__all__" filterset_fields = {
"application": ["isnull"],
"name": ["iexact"],
"authorization_flow__slug": ["iexact"],
"property_mappings": ["iexact"],
"internal_host": ["iexact"],
"external_host": ["iexact"],
"internal_host_ssl_validation": ["iexact"],
"certificate__kp_uuid": ["iexact"],
"certificate__name": ["iexact"],
"skip_path_regex": ["iexact"],
"basic_auth_enabled": ["iexact"],
"basic_auth_password_attribute": ["iexact"],
"basic_auth_user_attribute": ["iexact"],
"mode": ["iexact"],
"redirect_uris": ["iexact"],
"cookie_domain": ["iexact"],
}
ordering = ["name"] ordering = ["name"]

View File

@ -22,13 +22,13 @@ class ProxyDockerController(DockerController):
for proxy_provider in ProxyProvider.objects.filter(outpost__in=[self.outpost]): for proxy_provider in ProxyProvider.objects.filter(outpost__in=[self.outpost]):
proxy_provider: ProxyProvider proxy_provider: ProxyProvider
external_host_name = urlparse(proxy_provider.external_host) external_host_name = urlparse(proxy_provider.external_host)
hosts.append(f"`{external_host_name}`") hosts.append(f"`{external_host_name.netloc}`")
traefik_name = f"ak-outpost-{self.outpost.pk.hex}" traefik_name = f"ak-outpost-{self.outpost.pk.hex}"
return { labels = super()._get_labels()
"traefik.enable": "true", labels["traefik.enable"] = "true"
f"traefik.http.routers.{traefik_name}-router.rule": f"Host({','.join(hosts)})", labels[f"traefik.http.routers.{traefik_name}-router.rule"] = f"Host({','.join(hosts)})"
f"traefik.http.routers.{traefik_name}-router.tls": "true", labels[f"traefik.http.routers.{traefik_name}-router.tls"] = "true"
f"traefik.http.routers.{traefik_name}-router.service": f"{traefik_name}-service", labels[f"traefik.http.routers.{traefik_name}-router.service"] = f"{traefik_name}-service"
f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.path": "/", labels[f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.path"] = "/"
f"traefik.http.services.{traefik_name}-service.loadbalancer.server.port": "4180", labels[f"traefik.http.services.{traefik_name}-service.loadbalancer.server.port"] = "4180"
} return labels

View File

@ -61,9 +61,10 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
have_hosts.sort() have_hosts.sort()
have_hosts_tls = [] have_hosts_tls = []
for tls_config in current.spec.tls: if current.spec.tls:
if tls_config and tls_config.hosts: for tls_config in current.spec.tls:
have_hosts_tls += tls_config.hosts if tls_config and tls_config.hosts:
have_hosts_tls += tls_config.hosts
have_hosts_tls.sort() have_hosts_tls.sort()
if have_hosts != expected_hosts: if have_hosts != expected_hosts:

View File

@ -96,6 +96,7 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
def get_reference_object(self) -> TraefikMiddleware: def get_reference_object(self) -> TraefikMiddleware:
"""Get deployment object for outpost""" """Get deployment object for outpost"""
port = 9000 if self.is_embedded else 4180
return TraefikMiddleware( return TraefikMiddleware(
apiVersion=f"{CRD_GROUP}/{CRD_VERSION}", apiVersion=f"{CRD_GROUP}/{CRD_VERSION}",
kind="Middleware", kind="Middleware",
@ -106,7 +107,7 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
), ),
spec=TraefikMiddlewareSpec( spec=TraefikMiddlewareSpec(
forwardAuth=TraefikMiddlewareSpecForwardAuth( forwardAuth=TraefikMiddlewareSpecForwardAuth(
address=f"http://{self.name}.{self.namespace}:4180/akprox/auth?traefik", address=f"http://{self.name}.{self.namespace}:{port}/akprox/auth?traefik",
authResponseHeaders=[ authResponseHeaders=[
"Set-Cookie", "Set-Cookie",
"X-Auth-Username", "X-Auth-Username",

View File

@ -19,22 +19,24 @@ class ASGILogger:
app: ASGIApp app: ASGIApp
status_code: int
start: float
def __init__(self, app: ASGIApp): def __init__(self, app: ASGIApp):
self.app = app self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
content_length = 0 content_length = 0
status_code = 0
request_id = "" request_id = ""
location = ""
start = time()
async def send_hooked(message: Message) -> None: async def send_hooked(message: Message) -> None:
"""Hooked send method, which records status code and content-length, and for the final """Hooked send method, which records status code and content-length, and for the final
requests logs it""" requests logs it"""
headers = dict(message.get("headers", [])) headers = dict(message.get("headers", []))
if "status" in message: if "status" in message:
self.status_code = message["status"] nonlocal status_code
status_code = message["status"]
if b"Content-Length" in headers: if b"Content-Length" in headers:
nonlocal content_length nonlocal content_length
@ -43,14 +45,19 @@ class ASGILogger:
if message["type"] == "http.response.start": if message["type"] == "http.response.start":
response_headers = dict(message["headers"]) response_headers = dict(message["headers"])
nonlocal request_id nonlocal request_id
nonlocal location
request_id = response_headers.get(RESPONSE_HEADER_ID.encode(), b"").decode() request_id = response_headers.get(RESPONSE_HEADER_ID.encode(), b"").decode()
location = response_headers.get(b"Location", b"").decode()
if message["type"] == "http.response.body" and not message.get("more_body", True): if message["type"] == "http.response.body" and not message.get("more_body", True):
runtime = int((time() - self.start) * 1000) nonlocal start
self.log(scope, runtime, content_length, request_id=request_id) runtime = int((time() - start) * 1000)
kwargs = {"request_id": request_id}
if location != "":
kwargs["location"] = location
self.log(scope, runtime, content_length, status_code, **kwargs)
await send(message) await send(message)
self.start = time()
if scope["type"] == "lifespan": if scope["type"] == "lifespan":
# https://code.djangoproject.com/ticket/31508 # https://code.djangoproject.com/ticket/31508
# https://github.com/encode/uvicorn/issues/266 # https://github.com/encode/uvicorn/issues/266
@ -68,7 +75,7 @@ class ASGILogger:
# Check if header has multiple values, and use the first one # Check if header has multiple values, and use the first one
return client_ip.split(", ")[0] return client_ip.split(", ")[0]
def log(self, scope: Scope, content_length: int, runtime: float, **kwargs): def log(self, scope: Scope, content_length: int, runtime: float, status_code: int, **kwargs):
"""Outpot access logs in a structured format""" """Outpot access logs in a structured format"""
host = self._get_ip(scope) host = self._get_ip(scope)
query_string = "" query_string = ""
@ -79,7 +86,7 @@ class ASGILogger:
host=host, host=host,
method=scope.get("method", ""), method=scope.get("method", ""),
scheme=scope.get("scheme", ""), scheme=scope.get("scheme", ""),
status=self.status_code, status=status_code,
size=content_length / 1000 if content_length > 0 else 0, size=content_length / 1000 if content_length > 0 else 0,
runtime=runtime, runtime=runtime,
**kwargs, **kwargs,

View File

@ -24,7 +24,7 @@ class SessionMiddleware(UpstreamSessionMiddleware):
# Since go does not consider localhost with http a secure origin # Since go does not consider localhost with http a secure origin
# we can't set the secure flag. # we can't set the secure flag.
user_agent = request.META.get("HTTP_USER_AGENT", "") user_agent = request.META.get("HTTP_USER_AGENT", "")
if user_agent.startswith("authentik-outpost@"): if user_agent.startswith("authentik-outpost@") or "safari" in user_agent.lower():
return False return False
return True return True
return False return False

View File

@ -150,9 +150,20 @@ SPECTACULAR_SETTINGS = {
"DESCRIPTION": "Making authentication simple.", "DESCRIPTION": "Making authentication simple.",
"VERSION": __version__, "VERSION": __version__,
"COMPONENT_SPLIT_REQUEST": True, "COMPONENT_SPLIT_REQUEST": True,
"SCHEMA_PATH_PREFIX": "/api/v([0-9]+(beta)?)",
"SCHEMA_PATH_PREFIX_TRIM": True,
"SERVERS": [
{
"url": "/api/v3/",
},
{
"url": "/api/v2beta/",
},
],
"CONTACT": { "CONTACT": {
"email": "hello@beryju.org", "email": "hello@beryju.org",
}, },
"AUTHENTICATION_WHITELIST": ["authentik.api.authentication.TokenAuthentication"],
"LICENSE": { "LICENSE": {
"name": "GNU GPLv3", "name": "GNU GPLv3",
"url": "https://github.com/goauthentik/authentik/blob/master/LICENSE", "url": "https://github.com/goauthentik/authentik/blob/master/LICENSE",
@ -180,6 +191,9 @@ REST_FRAMEWORK = {
"rest_framework.filters.OrderingFilter", "rest_framework.filters.OrderingFilter",
"rest_framework.filters.SearchFilter", "rest_framework.filters.SearchFilter",
], ],
"DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser",
],
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.DjangoObjectPermissions",), "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.DjangoObjectPermissions",),
"DEFAULT_AUTHENTICATION_CLASSES": ( "DEFAULT_AUTHENTICATION_CLASSES": (
"authentik.api.authentication.TokenAuthentication", "authentik.api.authentication.TokenAuthentication",
@ -189,6 +203,7 @@ REST_FRAMEWORK = {
"rest_framework.renderers.JSONRenderer", "rest_framework.renderers.JSONRenderer",
], ],
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"TEST_REQUEST_DEFAULT_FORMAT": "json",
} }
REDIS_PROTOCOL_PREFIX = "redis://" REDIS_PROTOCOL_PREFIX = "redis://"
@ -357,7 +372,7 @@ CELERY_RESULT_BACKEND = (
# Database backup # Database backup
DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage" DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage"
DBBACKUP_STORAGE_OPTIONS = {"location": "./backups" if DEBUG else "/backups"} DBBACKUP_STORAGE_OPTIONS = {"location": "./backups" if DEBUG else "/backups"}
DBBACKUP_FILENAME_TEMPLATE = "authentik-backup-{datetime}.sql" DBBACKUP_FILENAME_TEMPLATE = f"authentik-backup-{__version__}-{{datetime}}.sql"
DBBACKUP_CONNECTOR_MAPPING = { DBBACKUP_CONNECTOR_MAPPING = {
"django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector", "django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
} }
@ -372,6 +387,7 @@ if CONFIG.y("postgresql.s3_backup"):
"default_acl": "private", "default_acl": "private",
"endpoint_url": CONFIG.y("postgresql.s3_backup.host"), "endpoint_url": CONFIG.y("postgresql.s3_backup.host"),
"location": CONFIG.y("postgresql.s3_backup.location", ""), "location": CONFIG.y("postgresql.s3_backup.location", ""),
"verify": not CONFIG.y_bool("postgresql.s3_backup.insecure_skip_verify", False),
} }
j_print( j_print(
"Database backup to S3 is configured", "Database backup to S3 is configured",

View File

@ -2,17 +2,13 @@
from base64 import b64encode from base64 import b64encode
from django.conf import settings from django.conf import settings
from django.test import Client, TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
class TestRoot(TestCase): class TestRoot(TestCase):
"""Test root application""" """Test root application"""
def setUp(self):
super().setUp()
self.client = Client()
def test_monitoring_error(self): def test_monitoring_error(self):
"""Test monitoring without any credentials""" """Test monitoring without any credentials"""
response = self.client.get(reverse("metrics")) response = self.client.get(reverse("metrics"))

View File

@ -43,7 +43,6 @@ class BaseOAuthClient:
profile_url = self.source.profile_url profile_url = self.source.profile_url
try: try:
response = self.do_request("get", profile_url, token=token) response = self.do_request("get", profile_url, token=token)
LOGGER.debug(response.text)
response.raise_for_status() response.raise_for_status()
except RequestException as exc: except RequestException as exc:
LOGGER.warning("Unable to fetch user profile", exc=exc) LOGGER.warning("Unable to fetch user profile", exc=exc)

View File

@ -65,7 +65,6 @@ class OAuth2Client(BaseOAuthClient):
data=args, data=args,
headers=self._default_headers, headers=self._default_headers,
) )
LOGGER.debug(response.text)
response.raise_for_status() response.raise_for_status()
except RequestException as exc: except RequestException as exc:
LOGGER.warning("Unable to fetch access token", exc=exc) LOGGER.warning("Unable to fetch access token", exc=exc)

View File

@ -1,5 +1,5 @@
"""Twitter Type tests""" """Twitter Type tests"""
from django.test import Client, TestCase from django.test import TestCase
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.twitter import TwitterOAuthCallback from authentik.sources.oauth.types.twitter import TwitterOAuthCallback
@ -92,7 +92,6 @@ class TestTypeGitHub(TestCase):
"""OAuth Source tests""" """OAuth Source tests"""
def setUp(self): def setUp(self):
self.client = Client()
self.source = OAuthSource.objects.create( self.source = OAuthSource.objects.create(
name="test", name="test",
slug="test", slug="test",

View File

@ -36,7 +36,6 @@ class AzureADClient(OAuth2Client):
profile_url, profile_url,
headers={"Authorization": f"{token['token_type']} {token['access_token']}"}, headers={"Authorization": f"{token['token_type']} {token['access_token']}"},
) )
LOGGER.debug(response.text)
response.raise_for_status() response.raise_for_status()
except RequestException as exc: except RequestException as exc:
LOGGER.warning("Unable to fetch user profile", exc=exc) LOGGER.warning("Unable to fetch user profile", exc=exc)

View File

@ -51,20 +51,30 @@ def get_webauthn_challenge(request: HttpRequest, device: WebAuthnDevice) -> dict
# for the reasons outlined in the comment in webauthn_begin_activate. # for the reasons outlined in the comment in webauthn_begin_activate.
request.session["challenge"] = challenge.rstrip("=") request.session["challenge"] = challenge.rstrip("=")
webauthn_user = WebAuthnUser( assertion = {}
device.user.uid, user = device.user
device.user.username,
device.user.name,
device.user.avatar,
device.credential_id,
device.public_key,
device.sign_count,
device.rp_id,
)
webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge) # We want all the user's WebAuthn devices and merge their challenges
for user_device in WebAuthnDevice.objects.filter(user=device.user).order_by("name"):
webauthn_user = WebAuthnUser(
user.uid,
user.username,
user.name,
user.avatar,
user_device.credential_id,
user_device.public_key,
user_device.sign_count,
user_device.rp_id,
)
webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge)
if assertion == {}:
assertion = webauthn_assertion_options.assertion_dict
else:
assertion["allowCredentials"] += webauthn_assertion_options.assertion_dict.get(
"allowCredentials"
)
return webauthn_assertion_options.assertion_dict return assertion
def validate_challenge_code(code: str, request: HttpRequest, user: User) -> str: def validate_challenge_code(code: str, request: HttpRequest, user: User) -> str:

View File

@ -20,8 +20,6 @@ from authentik.stages.authenticator_validate.models import AuthenticatorValidate
LOGGER = get_logger() LOGGER = get_logger()
PER_DEVICE_CLASSES = [DeviceClasses.WEBAUTHN]
class AuthenticatorValidationChallenge(WithUserInfoChallenge): class AuthenticatorValidationChallenge(WithUserInfoChallenge):
"""Authenticator challenge""" """Authenticator challenge"""
@ -91,9 +89,9 @@ class AuthenticatorValidateStageView(ChallengeStageView):
if device_class not in stage.device_classes: if device_class not in stage.device_classes:
LOGGER.debug("device class not allowed", device_class=device_class) LOGGER.debug("device class not allowed", device_class=device_class)
continue continue
# Ensure only classes in PER_DEVICE_CLASSES are returned per device # Ensure only one challenge per device class
# otherwise only return a single challenge # WebAuthn does another device loop to find all webuahtn devices
if device_class in seen_classes and device_class not in PER_DEVICE_CLASSES: if device_class in seen_classes:
continue continue
if device_class not in seen_classes: if device_class not in seen_classes:
seen_classes.append(device_class) seen_classes.append(device_class)

View File

@ -1,12 +1,12 @@
"""Test validator stage""" """Test validator stage"""
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.urls.base import reverse from django.urls.base import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_totp.models import TOTPDevice
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
@ -26,7 +26,7 @@ from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
from authentik.stages.identification.models import IdentificationStage, UserFields from authentik.stages.identification.models import IdentificationStage, UserFields
class AuthenticatorValidateStageTests(TestCase): class AuthenticatorValidateStageTests(APITestCase):
"""Test validator stage""" """Test validator stage"""
def setUp(self) -> None: def setUp(self) -> None:

View File

@ -1,7 +1,7 @@
"""captcha tests""" """captcha tests"""
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
@ -16,13 +16,12 @@ RECAPTCHA_PUBLIC_KEY = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"
RECAPTCHA_PRIVATE_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" RECAPTCHA_PRIVATE_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"
class TestCaptchaStage(TestCase): class TestCaptchaStage(APITestCase):
"""Captcha tests""" """Captcha tests"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.user = User.objects.create_user(username="unittest", email="test@beryju.org") self.user = User.objects.create_user(username="unittest", email="test@beryju.org")
self.client = Client()
self.flow = Flow.objects.create( self.flow = Flow.objects.create(
name="test-captcha", name="test-captcha",

View File

@ -1,9 +1,9 @@
"""consent tests""" """consent tests"""
from time import sleep from time import sleep
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import Application, User from authentik.core.models import Application, User
from authentik.core.tasks import clean_expired_models from authentik.core.tasks import clean_expired_models
@ -15,7 +15,7 @@ from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent
class TestConsentStage(TestCase): class TestConsentStage(APITestCase):
"""Consent tests""" """Consent tests"""
def setUp(self): def setUp(self):
@ -25,7 +25,6 @@ class TestConsentStage(TestCase):
name="test-application", name="test-application",
slug="test-application", slug="test-application",
) )
self.client = Client()
def test_always_required(self): def test_always_required(self):
"""Test always required consent""" """Test always required consent"""

View File

@ -1,7 +1,7 @@
"""deny tests""" """deny tests"""
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
@ -12,13 +12,12 @@ from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.deny.models import DenyStage from authentik.stages.deny.models import DenyStage
class TestUserDenyStage(TestCase): class TestUserDenyStage(APITestCase):
"""Deny tests""" """Deny tests"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.user = User.objects.create(username="unittest", email="test@beryju.org") self.user = User.objects.create(username="unittest", email="test@beryju.org")
self.client = Client()
self.flow = Flow.objects.create( self.flow = Flow.objects.create(
name="test-logout", name="test-logout",

View File

@ -1,7 +1,7 @@
"""dummy tests""" """dummy tests"""
from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
@ -9,7 +9,7 @@ from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.stages.dummy.models import DummyStage from authentik.stages.dummy.models import DummyStage
class TestDummyStage(TestCase): class TestDummyStage(APITestCase):
"""Dummy tests""" """Dummy tests"""
def setUp(self): def setUp(self):

View File

@ -4,8 +4,8 @@ from unittest.mock import MagicMock, patch
from django.core import mail from django.core import mail
from django.core.mail.backends.locmem import EmailBackend from django.core.mail.backends.locmem import EmailBackend
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
@ -16,13 +16,12 @@ from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
class TestEmailStageSending(TestCase): class TestEmailStageSending(APITestCase):
"""Email tests""" """Email tests"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.user = User.objects.create_user(username="unittest", email="test@beryju.org") self.user = User.objects.create_user(username="unittest", email="test@beryju.org")
self.client = Client()
self.flow = Flow.objects.create( self.flow = Flow.objects.create(
name="test-email", name="test-email",

View File

@ -2,10 +2,10 @@
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from django.core import mail from django.core import mail
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.http import urlencode from django.utils.http import urlencode
from rest_framework.test import APITestCase
from authentik.core.models import Token, User from authentik.core.models import Token, User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
@ -17,13 +17,12 @@ from authentik.stages.email.models import EmailStage
from authentik.stages.email.stage import QS_KEY_TOKEN from authentik.stages.email.stage import QS_KEY_TOKEN
class TestEmailStage(TestCase): class TestEmailStage(APITestCase):
"""Email tests""" """Email tests"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.user = User.objects.create_user(username="unittest", email="test@beryju.org") self.user = User.objects.create_user(username="unittest", email="test@beryju.org")
self.client = Client()
self.flow = Flow.objects.create( self.flow = Flow.objects.create(
name="test-email", name="test-email",

View File

@ -96,7 +96,9 @@ class IdentificationChallengeResponse(ChallengeResponse):
# No password stage select, don't validate the password # No password stage select, don't validate the password
return attrs return attrs
password = attrs["password"] password = attrs.get("password", None)
if not password:
LOGGER.warning("Password not set for ident+auth attempt")
try: try:
user = authenticate( user = authenticate(
self.stage.request, self.stage.request,
@ -132,6 +134,9 @@ class IdentificationStageView(ChallengeStageView):
else: else:
model_field += "__exact" model_field += "__exact"
query |= Q(**{model_field: uid_value}) query |= Q(**{model_field: uid_value})
if not query:
LOGGER.debug("Empty user query", query=query)
return None
users = User.objects.filter(query, is_active=True) users = User.objects.filter(query, is_active=True)
if users.exists(): if users.exists():
LOGGER.debug("Found user", user=users.first(), query=query) LOGGER.debug("Found user", user=users.first(), query=query)

View File

@ -1,7 +1,7 @@
"""identification tests""" """identification tests"""
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
@ -13,7 +13,7 @@ from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.models import PasswordStage from authentik.stages.password.models import PasswordStage
class TestIdentificationStage(TestCase): class TestIdentificationStage(APITestCase):
"""Identification tests""" """Identification tests"""
def setUp(self): def setUp(self):
@ -22,7 +22,6 @@ class TestIdentificationStage(TestCase):
self.user = User.objects.create_user( self.user = User.objects.create_user(
username="unittest", email="test@beryju.org", password=self.password username="unittest", email="test@beryju.org", password=self.password
) )
self.client = Client()
# OAuthSource for the login view # OAuthSource for the login view
source = OAuthSource.objects.create(name="test", slug="test") source = OAuthSource.objects.create(name="test", slug="test")
@ -137,6 +136,48 @@ class TestIdentificationStage(TestCase):
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_invalid_no_fields(self):
"""Test invalid with username (no user fields are enabled)"""
self.stage.user_fields = []
self.stage.save()
form_data = {"uid_field": self.user.username}
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
form_data,
)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-identification",
"password_fields": False,
"primary_action": "Log in",
"response_errors": {
"non_field_errors": [
{"code": "invalid", "string": "Failed to " "authenticate."}
]
},
"flow_info": {
"background": self.flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
"title": "",
},
"sources": [
{
"challenge": {
"component": "xak-flow-redirect",
"to": "/source/oauth/login/test/",
"type": ChallengeTypes.REDIRECT.value,
},
"icon_url": "/static/authentik/sources/.svg",
"name": "test",
}
],
"user_fields": [],
},
)
def test_invalid_with_invalid_email(self): def test_invalid_with_invalid_email(self):
"""Test with invalid email (user doesn't exist) -> Will return to login form""" """Test with invalid email (user doesn't exist) -> Will return to login form"""
form_data = {"uid_field": self.user.email + "test"} form_data = {"uid_field": self.user.email + "test"}

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.6 on 2021-09-01 12:11
from django.db import migrations, models
import authentik.core.models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_invitation", "0004_invitation_single_use"),
]
operations = [
migrations.AddField(
model_name="invitation",
name="expiring",
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name="invitation",
name="expires",
field=models.DateTimeField(default=authentik.core.models.default_token_duration),
),
]

View File

@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
from authentik.core.models import User from authentik.core.models import ExpiringModel, User
from authentik.flows.models import Stage from authentik.flows.models import Stage
@ -48,7 +48,7 @@ class InvitationStage(Stage):
verbose_name_plural = _("Invitation Stages") verbose_name_plural = _("Invitation Stages")
class Invitation(models.Model): class Invitation(ExpiringModel):
"""Single-use invitation link""" """Single-use invitation link"""
invite_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) invite_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@ -59,7 +59,6 @@ class Invitation(models.Model):
) )
created_by = models.ForeignKey(User, on_delete=models.CASCADE) created_by = models.ForeignKey(User, on_delete=models.CASCADE)
expires = models.DateTimeField(default=None, blank=True, null=True)
fixed_data = models.JSONField( fixed_data = models.JSONField(
default=dict, default=dict,
blank=True, blank=True,

View File

@ -1,7 +1,6 @@
"""invitation tests""" """invitation tests"""
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.http import urlencode from django.utils.http import urlencode
@ -21,13 +20,12 @@ from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
class TestUserLoginStage(TestCase): class TestUserLoginStage(APITestCase):
"""Login tests""" """Login tests"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.user = User.objects.create(username="unittest", email="test@beryju.org") self.user = User.objects.create(username="unittest", email="test@beryju.org")
self.client = Client()
self.flow = Flow.objects.create( self.flow = Flow.objects.create(
name="test-invitation", name="test-invitation",

View File

@ -32,9 +32,7 @@ PLAN_CONTEXT_METHOD_ARGS = "auth_method_args"
SESSION_INVALID_TRIES = "user_invalid_tries" SESSION_INVALID_TRIES = "user_invalid_tries"
def authenticate( def authenticate(request: HttpRequest, backends: list[str], **credentials: Any) -> Optional[User]:
request: HttpRequest, backends: list[str], **credentials: dict[str, Any]
) -> Optional[User]:
"""If the given credentials are valid, return a User object. """If the given credentials are valid, return a User object.
Customized version of django's authenticate, which accepts a list of backends""" Customized version of django's authenticate, which accepts a list of backends"""

View File

@ -2,9 +2,9 @@
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
@ -20,7 +20,7 @@ from authentik.stages.password.models import PasswordStage
MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test")) MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test"))
class TestPasswordStage(TestCase): class TestPasswordStage(APITestCase):
"""Password tests""" """Password tests"""
def setUp(self): def setUp(self):
@ -29,7 +29,6 @@ class TestPasswordStage(TestCase):
self.user = User.objects.create_user( self.user = User.objects.create_user(
username="unittest", email="test@beryju.org", password=self.password username="unittest", email="test@beryju.org", password=self.password
) )
self.client = Client()
self.flow = Flow.objects.create( self.flow = Flow.objects.create(
name="test-password", name="test-password",

View File

@ -1,10 +1,10 @@
"""Prompt tests""" """Prompt tests"""
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework.exceptions import ErrorDetail from rest_framework.exceptions import ErrorDetail
from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
@ -17,13 +17,12 @@ from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT, PromptChallengeResponse from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT, PromptChallengeResponse
class TestPromptStage(TestCase): class TestPromptStage(APITestCase):
"""Prompt tests""" """Prompt tests"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.user = User.objects.create(username="unittest", email="test@beryju.org") self.user = User.objects.create(username="unittest", email="test@beryju.org")
self.client = Client()
self.flow = Flow.objects.create( self.flow = Flow.objects.create(
name="test-prompt", name="test-prompt",
@ -97,7 +96,6 @@ class TestPromptStage(TestCase):
static_prompt, static_prompt,
] ]
) )
self.stage.save()
self.prompt_data = { self.prompt_data = {
username_prompt.field_key: "test-username", username_prompt.field_key: "test-username",

View File

@ -1,9 +1,9 @@
"""delete tests""" """delete tests"""
from unittest.mock import patch from unittest.mock import patch
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
@ -15,14 +15,13 @@ from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.user_delete.models import UserDeleteStage from authentik.stages.user_delete.models import UserDeleteStage
class TestUserDeleteStage(TestCase): class TestUserDeleteStage(APITestCase):
"""Delete tests""" """Delete tests"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.username = "qerqwerqrwqwerwq" self.username = "qerqwerqrwqwerwq"
self.user = User.objects.create(username=self.username, email="test@beryju.org") self.user = User.objects.create(username=self.username, email="test@beryju.org")
self.client = Client()
self.flow = Flow.objects.create( self.flow = Flow.objects.create(
name="test-delete", name="test-delete",

View File

@ -2,9 +2,9 @@
from time import sleep from time import sleep
from unittest.mock import patch from unittest.mock import patch
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
@ -16,13 +16,12 @@ from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.user_login.models import UserLoginStage from authentik.stages.user_login.models import UserLoginStage
class TestUserLoginStage(TestCase): class TestUserLoginStage(APITestCase):
"""Login tests""" """Login tests"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.user = User.objects.create(username="unittest", email="test@beryju.org") self.user = User.objects.create(username="unittest", email="test@beryju.org")
self.client = Client()
self.flow = Flow.objects.create( self.flow = Flow.objects.create(
name="test-login", name="test-login",

View File

@ -1,7 +1,7 @@
"""logout tests""" """logout tests"""
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
@ -14,13 +14,12 @@ from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from authentik.stages.user_logout.models import UserLogoutStage from authentik.stages.user_logout.models import UserLogoutStage
class TestUserLogoutStage(TestCase): class TestUserLogoutStage(APITestCase):
"""Logout tests""" """Logout tests"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.user = User.objects.create(username="unittest", email="test@beryju.org") self.user = User.objects.create(username="unittest", email="test@beryju.org")
self.client = Client()
self.flow = Flow.objects.create( self.flow = Flow.objects.create(
name="test-logout", name="test-logout",

View File

@ -3,9 +3,9 @@ import string
from random import SystemRandom from random import SystemRandom
from unittest.mock import patch from unittest.mock import patch
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import USER_ATTRIBUTE_SOURCES, Source, User, UserSourceConnection from authentik.core.models import USER_ATTRIBUTE_SOURCES, Source, User, UserSourceConnection
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
@ -19,13 +19,11 @@ from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
from authentik.stages.user_write.models import UserWriteStage from authentik.stages.user_write.models import UserWriteStage
class TestUserWriteStage(TestCase): class TestUserWriteStage(APITestCase):
"""Write tests""" """Write tests"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.client = Client()
self.flow = Flow.objects.create( self.flow = Flow.objects.create(
name="test-write", name="test-write",
slug="test-write", slug="test-write",

View File

@ -1,120 +0,0 @@
trigger:
batch: true
branches:
include:
- master
- next
- version-*
stages:
- stage: generate
jobs:
- job: generate_api
pool:
vmImage: 'ubuntu-latest'
steps:
- task: GoTool@0
inputs:
version: '1.16.3'
- task: CmdLine@2
inputs:
script: make gen-outpost
- task: PublishPipelineArtifact@1
inputs:
targetPath: 'api/'
artifact: 'go_api_client'
publishLocation: 'pipeline'
- stage: lint
jobs:
- job: golint
pool:
vmImage: 'ubuntu-latest'
steps:
- task: GoTool@0
inputs:
version: '1.16.3'
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'go_api_client'
path: "api/"
- task: CmdLine@2
inputs:
script: |
mkdir -p web/dist
mkdir -p website/help
touch web/dist/test website/help/test
docker run \
--rm \
-v $(pwd):/app \
-w /app \
golangci/golangci-lint:v1.39.0 \
golangci-lint run -v --timeout 200s
- stage: build_docker
jobs:
- job: proxy_build_docker
pool:
vmImage: 'ubuntu-latest'
steps:
- task: GoTool@0
inputs:
version: '1.16.3'
- task: Bash@3
inputs:
targetType: 'inline'
script: |
python ./scripts/az_do_set_branch.py
- task: Docker@2
inputs:
containerRegistry: 'beryjuorg-harbor'
repository: 'authentik/outpost-proxy'
command: 'build'
Dockerfile: 'proxy.Dockerfile'
buildContext: '$(Build.SourcesDirectory)'
tags: |
gh-$(branchName)
gh-$(branchName)-$(timestamp)
gh-$(Build.SourceVersion)
arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)'
- task: Docker@2
inputs:
containerRegistry: 'beryjuorg-harbor'
repository: 'authentik/outpost-proxy'
command: 'push'
tags: |
gh-$(branchName)
gh-$(branchName)-$(timestamp)
gh-$(Build.SourceVersion)
- job: ldap_build_docker
pool:
vmImage: 'ubuntu-latest'
steps:
- task: GoTool@0
inputs:
version: '1.16.3'
- task: Bash@3
inputs:
targetType: 'inline'
script: |
python ./scripts/az_do_set_branch.py
- task: Docker@2
inputs:
containerRegistry: 'beryjuorg-harbor'
repository: 'authentik/outpost-ldap'
command: 'build'
Dockerfile: 'ldap.Dockerfile'
buildContext: '$(Build.SourcesDirectory)'
tags: |
gh-$(branchName)
gh-$(branchName)-$(timestamp)
gh-$(Build.SourceVersion)
arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)'
- task: Docker@2
inputs:
containerRegistry: 'beryjuorg-harbor'
repository: 'authentik/outpost-ldap'
command: 'push'
tags: |
gh-$(branchName)
gh-$(branchName)-$(timestamp)
gh-$(Build.SourceVersion)

View File

@ -1,426 +0,0 @@
trigger:
batch: true
branches:
include:
- master
- next
- version-*
paths:
exclude:
- website
- outpost
resources:
- repo: self
variables:
- name: POSTGRES_DB
value: authentik
- name: POSTGRES_USER
value: authentik
- name: POSTGRES_PASSWORD
value: "EK-5jnKfjrGRm<77"
- group: coverage
stages:
- stage: Lint_and_test
jobs:
- job: pylint
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.9'
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/ci.docker-compose.yml'
action: 'Run services'
buildImages: false
- task: CmdLine@2
inputs:
script: |
sudo apt update
sudo apt install -y libxmlsec1-dev pkg-config
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: |
pipenv run python -m scripts.generate_ci_config
pipenv run pylint authentik tests lifecycle
- job: black
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.9'
- task: CmdLine@2
inputs:
script: |
sudo apt update
sudo apt install -y libxmlsec1-dev pkg-config
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run black --check authentik tests lifecycle
- job: isort
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.9'
- task: CmdLine@2
inputs:
script: |
sudo apt update
sudo apt install -y libxmlsec1-dev pkg-config
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run isort --check authentik tests lifecycle
- job: bandit
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.9'
- task: CmdLine@2
inputs:
script: |
sudo apt update
sudo apt install -y libxmlsec1-dev pkg-config
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run bandit -r authentik tests lifecycle
- job: pyright
pool:
vmImage: ubuntu-latest
steps:
- task: UseNode@1
inputs:
version: '12.x'
- task: UsePythonVersion@0
inputs:
versionSpec: '3.9'
- task: CmdLine@2
inputs:
script: npm install -g pyright@1.1.136
- task: CmdLine@2
inputs:
script: |
sudo apt update
sudo apt install -y libxmlsec1-dev pkg-config
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run pyright e2e lifecycle
- job: migrations
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.9'
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/ci.docker-compose.yml'
action: 'Run services'
buildImages: false
- task: CmdLine@2
inputs:
script: |
sudo apt update
sudo apt install -y libxmlsec1-dev pkg-config
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: |
pipenv run python -m scripts.generate_ci_config
pipenv run python -m lifecycle.migrate
- job: migrations_from_previous_release
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: UsePythonVersion@0
inputs:
versionSpec: '3.9'
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/ci.docker-compose.yml'
action: 'Run services'
buildImages: false
- task: CmdLine@2
displayName: Prepare Last tagged release
inputs:
script: |
# Copy current, latest config to local
cp authentik/lib/default.yml local.env.yml
git checkout $(git describe --abbrev=0 --match 'version/*')
sudo apt update
sudo apt install -y libxmlsec1-dev pkg-config
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
displayName: Migrate to last tagged release
inputs:
script: |
pipenv run python -m scripts.generate_ci_config
pipenv run python -m lifecycle.migrate
- task: CmdLine@2
displayName: Install current branch
inputs:
script: |
set -x
git checkout ${{ variables.branchName }}
pipenv sync --dev
- task: CmdLine@2
displayName: Migrate to current branch
inputs:
script: |
pipenv run python -m scripts.generate_ci_config
pipenv run python -m lifecycle.migrate
- job: coverage_unittest
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.9'
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/ci.docker-compose.yml'
action: 'Run services'
buildImages: false
- task: CmdLine@2
inputs:
script: |
sudo apt update
sudo apt install -y libxmlsec1-dev pkg-config
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
displayName: Run full test suite
inputs:
script: |
pipenv run python -m scripts.generate_ci_config
pipenv run make test
- task: CmdLine@2
inputs:
script: |
mkdir output-unittest
mv unittest.xml output-unittest/unittest.xml
mv .coverage output-unittest/coverage
- task: PublishPipelineArtifact@1
inputs:
targetPath: 'output-unittest/'
artifact: 'coverage-unittest'
publishLocation: 'pipeline'
- job: coverage_integration
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.9'
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/ci.docker-compose.yml'
action: 'Run services'
buildImages: false
- task: CmdLine@2
inputs:
script: |
sudo apt update
sudo apt install -y libxmlsec1-dev pkg-config
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
displayName: Install K3d and prepare
inputs:
script: |
wget -q -O - https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash
k3d cluster create
k3d kubeconfig write -o ~/.kube/config --overwrite
- task: CmdLine@2
displayName: Run full test suite
inputs:
script: |
pipenv run python -m scripts.generate_ci_config
pipenv run make test-integration
- task: CmdLine@2
inputs:
script: |
mkdir output-integration
mv unittest.xml output-integration/unittest.xml
mv .coverage output-integration/coverage
- task: PublishPipelineArtifact@1
inputs:
targetPath: 'output-integration/'
artifact: 'coverage-integration'
publishLocation: 'pipeline'
- job: coverage_e2e
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.9'
- task: NodeTool@0
inputs:
versionSpec: '16.x'
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/ci.docker-compose.yml'
action: 'Run services'
buildImages: false
- task: CmdLine@2
inputs:
script: |
sudo apt update
sudo apt install -y libxmlsec1-dev pkg-config
sudo pip install -U wheel pipenv
pipenv install --dev --python python3.9
- task: DockerCompose@0
displayName: Run ChromeDriver
inputs:
dockerComposeFile: 'tests/e2e/ci.docker-compose.yml'
action: 'Run a specific service'
serviceName: 'chrome'
- task: CmdLine@2
displayName: Build static files for e2e
inputs:
script: |
cd web
npm i
npm run build
- task: CmdLine@2
displayName: Run full test suite
inputs:
script: |
pipenv run python -m scripts.generate_ci_config
pipenv run make test-e2e
- task: CmdLine@2
condition: always()
displayName: Cleanup
inputs:
script: |
docker stop $(docker ps -aq)
docker container prune -f
- task: CmdLine@2
displayName: Prepare unittests and coverage for upload
inputs:
script: |
mkdir output-e2e
mv unittest.xml output-e2e/unittest.xml
mv .coverage output-e2e/coverage
- task: PublishPipelineArtifact@1
condition: failed()
displayName: Upload screenshots if selenium tests fail
inputs:
targetPath: 'selenium_screenshots/'
artifact: 'selenium screenshots'
publishLocation: 'pipeline'
- task: PublishPipelineArtifact@1
inputs:
targetPath: 'output-e2e/'
artifact: 'coverage-e2e'
publishLocation: 'pipeline'
- stage: test_combine
jobs:
- job: test_coverage_combine
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'coverage-e2e'
path: "coverage-e2e/"
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'coverage-integration'
path: "coverage-integration/"
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'coverage-unittest'
path: "coverage-unittest/"
- task: UsePythonVersion@0
inputs:
versionSpec: '3.9'
- task: CmdLine@2
inputs:
script: |
sudo apt update
sudo apt install -y libxmlsec1-dev pkg-config
sudo pip install -U wheel pipenv
pipenv install --dev
pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage coverage-integration/coverage
pipenv run coverage xml
pipenv run coverage html
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: 'coverage.xml'
pathToSources: '$(System.DefaultWorkingDirectory)'
- task: PublishTestResults@2
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: |
coverage-e2e/unittest.xml
coverage-integration/unittest.xml
coverage-unittest/unittest.xml
mergeTestResults: true
- task: CmdLine@2
inputs:
script: bash <(curl -s https://codecov.io/bash)
- stage: Build
jobs:
- job: build_server
pool:
vmImage: 'ubuntu-latest'
steps:
- task: Bash@3
inputs:
targetType: 'inline'
script: |
python ./scripts/az_do_set_branch.py
- task: Docker@2
inputs:
containerRegistry: 'beryjuorg-harbor'
repository: 'authentik/server'
command: 'build'
Dockerfile: 'Dockerfile'
tags: |
gh-$(branchName)
gh-$(branchName)-$(timestamp)
arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)'
- task: Docker@2
inputs:
containerRegistry: 'beryjuorg-harbor'
repository: 'authentik/server'
command: 'push'
tags: |
gh-$(branchName)
gh-$(branchName)-$(timestamp)

View File

@ -60,7 +60,9 @@ func main() {
for { for {
go attemptStartBackend(g) go attemptStartBackend(g)
ws.Start() ws.Start()
go attemptProxyStart(ws, u) if !config.G.Web.DisableEmbeddedOutpost {
go attemptProxyStart(ws, u)
}
<-ex <-ex
running = false running = false

View File

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

2
go.mod
View File

@ -10,7 +10,7 @@ require (
github.com/go-ldap/ldap/v3 v3.4.1 github.com/go-ldap/ldap/v3 v3.4.1
github.com/go-openapi/analysis v0.20.1 // indirect github.com/go-openapi/analysis v0.20.1 // indirect
github.com/go-openapi/errors v0.20.0 // indirect github.com/go-openapi/errors v0.20.0 // indirect
github.com/go-openapi/runtime v0.19.30 github.com/go-openapi/runtime v0.19.31
github.com/go-openapi/strfmt v0.20.2 github.com/go-openapi/strfmt v0.20.2
github.com/go-openapi/swag v0.19.15 // indirect github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-openapi/validate v0.20.2 // indirect github.com/go-openapi/validate v0.20.2 // indirect

4
go.sum
View File

@ -205,8 +205,8 @@ github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29g
github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo=
github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98=
github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
github.com/go-openapi/runtime v0.19.30 h1:bVDeSf4HU9EMth+lHD1EthaHe1SFoUVPaUvQtkGS9g8= github.com/go-openapi/runtime v0.19.31 h1:GX+MgBxN12s/tQiHNJpvHDIoZiEXAz6j6Rqg0oJcnpg=
github.com/go-openapi/runtime v0.19.30/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M= github.com/go-openapi/runtime v0.19.31/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M=
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=

View File

@ -27,9 +27,10 @@ type RedisConfig struct {
} }
type WebConfig struct { type WebConfig struct {
Listen string `yaml:"listen"` Listen string `yaml:"listen"`
ListenTLS string `yaml:"listen_tls"` ListenTLS string `yaml:"listen_tls"`
LoadLocalFiles bool `yaml:"load_local_files" env:"AUTHENTIK_WEB_LOAD_LOCAL_FILES"` LoadLocalFiles bool `yaml:"load_local_files" env:"AUTHENTIK_WEB_LOAD_LOCAL_FILES"`
DisableEmbeddedOutpost bool `yaml:"disable_embedded_outpost" env:"AUTHENTIK_WEB__DISABLE_EMBEDDED_OUTPOST"`
} }
type PathsConfig struct { type PathsConfig struct {

View File

@ -17,4 +17,4 @@ func OutpostUserAgent() string {
return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD()) return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD())
} }
const VERSION = "2021.8.2" const VERSION = "2021.8.5"

View File

@ -47,7 +47,7 @@ func NewAPIController(akURL url.URL, token string) *APIController {
config.Host = akURL.Host config.Host = akURL.Host
config.Scheme = akURL.Scheme config.Scheme = akURL.Scheme
config.HTTPClient = &http.Client{ config.HTTPClient = &http.Client{
Transport: NewTracingTransport(GetTLSTransport()), Transport: NewTracingTransport(context.TODO(), GetTLSTransport()),
} }
config.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token)) config.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token))
@ -107,8 +107,24 @@ func (a *APIController) Start() error {
return nil return nil
} }
func (a *APIController) OnRefresh() error {
// Because we don't know the outpost UUID, we simply do a list and pick the first
// The service account this token belongs to should only have access to a single outpost
outposts, _, err := a.Client.OutpostsApi.OutpostsInstancesList(context.Background()).Execute()
if err != nil {
log.WithError(err).Error("Failed to fetch outpost configuration")
return err
}
outpost := outposts.Results[0]
doGlobalSetup(outpost.Config)
log.WithField("name", outpost.Name).Debug("Fetched outpost configuration")
return a.Server.Refresh()
}
func (a *APIController) StartBackgorundTasks() error { func (a *APIController) StartBackgorundTasks() error {
err := a.Server.Refresh() err := a.OnRefresh()
if err != nil { if err != nil {
return errors.Wrap(err, "failed to run initial refresh") return errors.Wrap(err, "failed to run initial refresh")
} }

View File

@ -82,7 +82,7 @@ func (ac *APIController) startWSHandler() {
if wsMsg.Instruction == WebsocketInstructionTriggerUpdate { if wsMsg.Instruction == WebsocketInstructionTriggerUpdate {
time.Sleep(ac.reloadOffset) time.Sleep(ac.reloadOffset)
logger.Debug("Got update trigger...") logger.Debug("Got update trigger...")
err := ac.Server.Refresh() err := ac.OnRefresh()
if err != nil { if err != nil {
logger.WithError(err).Debug("Failed to update") logger.WithError(err).Debug("Failed to update")
} }
@ -118,7 +118,7 @@ func (ac *APIController) startIntervalUpdater() {
logger := ac.logger.WithField("loop", "interval-updater") logger := ac.logger.WithField("loop", "interval-updater")
ticker := time.NewTicker(5 * time.Minute) ticker := time.NewTicker(5 * time.Minute)
for ; true; <-ticker.C { for ; true; <-ticker.C {
err := ac.Server.Refresh() err := ac.OnRefresh()
if err != nil { if err != nil {
logger.WithError(err).Debug("Failed to update") logger.WithError(err).Debug("Failed to update")
} }

View File

@ -1,6 +1,7 @@
package ak package ak
import ( import (
"context"
"net/http" "net/http"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
@ -8,14 +9,15 @@ import (
type tracingTransport struct { type tracingTransport struct {
inner http.RoundTripper inner http.RoundTripper
ctx context.Context
} }
func NewTracingTransport(inner http.RoundTripper) *tracingTransport { func NewTracingTransport(ctx context.Context, inner http.RoundTripper) *tracingTransport {
return &tracingTransport{inner} return &tracingTransport{inner, ctx}
} }
func (tt *tracingTransport) RoundTrip(r *http.Request) (*http.Response, error) { func (tt *tracingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
span := sentry.StartSpan(r.Context(), "authentik.go.http_request") span := sentry.StartSpan(tt.ctx, "authentik.go.http_request")
span.SetTag("url", r.URL.String()) span.SetTag("url", r.URL.String())
span.SetTag("method", r.Method) span.SetTag("method", r.Method)
defer span.Finish() defer span.Finish()

View File

@ -16,6 +16,7 @@ import (
"goauthentik.io/api" "goauthentik.io/api"
"goauthentik.io/internal/constants" "goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ak" "goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/utils"
) )
type StageComponent string type StageComponent string
@ -61,8 +62,10 @@ func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Config
config.UserAgent = constants.OutpostUserAgent() config.UserAgent = constants.OutpostUserAgent()
config.HTTPClient = &http.Client{ config.HTTPClient = &http.Client{
Jar: jar, Jar: jar,
Transport: ak.NewTracingTransport(ak.GetTLSTransport()), Transport: ak.NewTracingTransport(ctx, ak.GetTLSTransport()),
} }
token := strings.Split(refConfig.DefaultHeader["Authorization"], " ")[1]
config.AddDefaultHeader(HeaderAuthentikOutpostToken, token)
apiClient := api.NewAPIClient(config) apiClient := api.NewAPIClient(config)
return &FlowExecutor{ return &FlowExecutor{
Params: url.Values{}, Params: url.Values{},
@ -71,7 +74,7 @@ func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Config
api: apiClient, api: apiClient,
flowSlug: flowSlug, flowSlug: flowSlug,
log: l, log: l,
token: strings.Split(refConfig.DefaultHeader["Authorization"], " ")[1], token: token,
sp: rsp, sp: rsp,
} }
} }
@ -87,13 +90,7 @@ type ChallengeInt interface {
} }
func (fe *FlowExecutor) DelegateClientIP(a net.Addr) { func (fe *FlowExecutor) DelegateClientIP(a net.Addr) {
host, _, err := net.SplitHostPort(a.String()) fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikRemoteIP, utils.GetIP(a))
if err != nil {
fe.log.WithError(err).Warning("Failed to get remote IP")
return
}
fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikRemoteIP, host)
fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikOutpostToken, fe.token)
} }
func (fe *FlowExecutor) CheckApplicationAccess(appSlug string) (bool, error) { func (fe *FlowExecutor) CheckApplicationAccess(appSlug string) (bool, error) {
@ -152,7 +149,9 @@ func (fe *FlowExecutor) solveFlowChallenge(depth int) (bool, error) {
responseReq := fe.api.FlowsApi.FlowsExecutorSolve(scsp.Context(), fe.flowSlug).Query(fe.Params.Encode()) responseReq := fe.api.FlowsApi.FlowsExecutorSolve(scsp.Context(), fe.flowSlug).Query(fe.Params.Encode())
switch ch.GetComponent() { switch ch.GetComponent() {
case string(StageIdentification): case string(StageIdentification):
responseReq = responseReq.FlowChallengeResponseRequest(api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewIdentificationChallengeResponseRequest(fe.getAnswer(StageIdentification)))) r := api.NewIdentificationChallengeResponseRequest(fe.getAnswer(StageIdentification))
r.SetPassword(fe.getAnswer(StagePassword))
responseReq = responseReq.FlowChallengeResponseRequest(api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(r))
case string(StagePassword): case string(StagePassword):
responseReq = responseReq.FlowChallengeResponseRequest(api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewPasswordChallengeResponseRequest(fe.getAnswer(StagePassword)))) responseReq = responseReq.FlowChallengeResponseRequest(api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewPasswordChallengeResponseRequest(fe.getAnswer(StagePassword))))
case string(StageAuthenticatorValidate): case string(StageAuthenticatorValidate):

View File

@ -9,6 +9,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/nmcclain/ldap" "github.com/nmcclain/ldap"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"goauthentik.io/internal/utils"
) )
type BindRequest struct { type BindRequest struct {
@ -33,7 +34,7 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD
BindDN: bindDN, BindDN: bindDN,
BindPW: bindPW, BindPW: bindPW,
conn: conn, conn: conn,
log: ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", conn.RemoteAddr().String()), log: ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", utils.GetIP(conn.RemoteAddr())),
id: rid, id: rid,
ctx: span.Context(), ctx: span.Context(),
} }

View File

@ -0,0 +1,32 @@
package ldap
import (
"net"
"time"
)
func (ls *LDAPServer) Close(boundDN string, conn net.Conn) error {
for _, p := range ls.providers {
p.delayDeleteUserInfo(boundDN)
}
return nil
}
func (pi *ProviderInstance) delayDeleteUserInfo(dn string) {
ticker := time.NewTicker(30 * time.Second)
quit := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
pi.boundUsersMutex.Lock()
delete(pi.boundUsers, dn)
pi.boundUsersMutex.Unlock()
close(quit)
case <-quit:
ticker.Stop()
return
}
}
}()
}

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"strings" "strings"
"time"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
goldap "github.com/go-ldap/ldap/v3" goldap "github.com/go-ldap/ldap/v3"
@ -12,6 +11,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"goauthentik.io/api" "goauthentik.io/api"
"goauthentik.io/internal/outpost" "goauthentik.io/internal/outpost"
"goauthentik.io/internal/utils"
) )
const ContextUserKey = "ak_user" const ContextUserKey = "ak_user"
@ -37,7 +37,7 @@ func (pi *ProviderInstance) getUsername(dn string) (string, error) {
func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPResultCode, error) { func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPResultCode, error) {
fe := outpost.NewFlowExecutor(req.ctx, pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{ fe := outpost.NewFlowExecutor(req.ctx, pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
"bindDN": req.BindDN, "bindDN": req.BindDN,
"client": req.conn.RemoteAddr().String(), "client": utils.GetIP(req.conn.RemoteAddr()),
"requestId": req.id, "requestId": req.id,
}) })
fe.DelegateClientIP(req.conn.RemoteAddr()) fe.DelegateClientIP(req.conn.RemoteAddr())
@ -83,7 +83,6 @@ func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPRes
} }
uisp.Finish() uisp.Finish()
defer pi.boundUsersMutex.Unlock() defer pi.boundUsersMutex.Unlock()
pi.delayDeleteUserInfo(username)
return ldap.LDAPResultSuccess, nil return ldap.LDAPResultSuccess, nil
} }
@ -100,25 +99,6 @@ func (pi *ProviderInstance) SearchAccessCheck(user api.UserSelf) *string {
return nil return nil
} }
func (pi *ProviderInstance) delayDeleteUserInfo(dn string) {
ticker := time.NewTicker(30 * time.Second)
quit := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
pi.boundUsersMutex.Lock()
delete(pi.boundUsers, dn)
pi.boundUsersMutex.Unlock()
close(quit)
case <-quit:
ticker.Stop()
return
}
}
}()
}
func (pi *ProviderInstance) TimerFlowCacheExpiry() { func (pi *ProviderInstance) TimerFlowCacheExpiry() {
fe := outpost.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{}) fe := outpost.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{})
fe.Params.Add("goauthentik.io/outpost/ldap", "true") fe.Params.Add("goauthentik.io/outpost/ldap", "true")

View File

@ -83,5 +83,6 @@ func NewServer(ac *ak.APIController) *LDAPServer {
ls.defaultCert = &defaultCert ls.defaultCert = &defaultCert
s.BindFunc("", ls) s.BindFunc("", ls)
s.SearchFunc("", ls) s.SearchFunc("", ls)
s.CloseFunc("", ls)
return ls return ls
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/nmcclain/ldap" "github.com/nmcclain/ldap"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"goauthentik.io/internal/utils"
) )
type SearchRequest struct { type SearchRequest struct {
@ -35,7 +36,7 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
SearchRequest: searchReq, SearchRequest: searchReq,
BindDN: bindDN, BindDN: bindDN,
conn: conn, conn: conn,
log: ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", conn.RemoteAddr().String()).WithField("filter", searchReq.Filter).WithField("baseDN", searchReq.BaseDN), log: ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", utils.GetIP(conn.RemoteAddr())).WithField("filter", searchReq.Filter).WithField("baseDN", searchReq.BaseDN),
id: rid, id: rid,
ctx: span.Context(), ctx: span.Context(),
} }

View File

@ -25,6 +25,7 @@ type providerBundle struct {
Host string Host string
endSessionUrl string endSessionUrl string
Mode *api.ProxyMode
cert *tls.Certificate cert *tls.Certificate
@ -38,8 +39,8 @@ func intToPointer(i int) *int {
func (pb *providerBundle) replaceLocal(url string) string { func (pb *providerBundle) replaceLocal(url string) string {
if strings.HasPrefix(url, "http://localhost:8000") { if strings.HasPrefix(url, "http://localhost:8000") {
authentikHost, c := pb.s.ak.Outpost.Config["authentik_host"] authentikHost, c := pb.s.ak.Outpost.Config["authentik_host"]
if !c { if !c || authentikHost == "" {
pb.log.Warning("Outpost has localhost API Connection but no authentik_host is configured.") pb.log.Warning("Outpost has localhost/blank API Connection but no authentik_host is configured.")
return url return url
} }
f := strings.ReplaceAll(url, "http://localhost:8000", authentikHost.(string)) f := strings.ReplaceAll(url, "http://localhost:8000", authentikHost.(string))
@ -49,6 +50,10 @@ func (pb *providerBundle) replaceLocal(url string) string {
} }
func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.Options { func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.Options {
// We need to save the mode in the bundle
// Since for the embedded outpost we only switch for fully proxy providers
pb.Mode = provider.Mode
externalHost, err := url.Parse(provider.ExternalHost) externalHost, err := url.Parse(provider.ExternalHost)
if err != nil { if err != nil {
log.WithError(err).Warning("Failed to parse URL, skipping provider") log.WithError(err).Warning("Failed to parse URL, skipping provider")

View File

@ -22,6 +22,7 @@ import (
"github.com/oauth2-proxy/oauth2-proxy/providers" "github.com/oauth2-proxy/oauth2-proxy/providers"
"goauthentik.io/api" "goauthentik.io/api"
"goauthentik.io/internal/utils/web" "goauthentik.io/internal/utils/web"
staticWeb "goauthentik.io/web"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -121,7 +122,7 @@ func NewOAuthProxy(opts *options.Options, provider api.ProxyOutpostConfig, c *ht
redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix) redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
} }
logger.Printf("proxy instance configured for Client ID: %s", opts.ClientID) logger.WithField("auth_url", opts.GetProvider().Data().LoginURL.String()).WithField("client_id", opts.ClientID).Info("proxy instance configured")
sessionChain := buildSessionChain(opts, sessionStore) sessionChain := buildSessionChain(opts, sessionStore)
@ -255,11 +256,18 @@ func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
p.AuthenticateOnly(rw, req) p.AuthenticateOnly(rw, req)
case path == p.UserInfoPath: case path == p.UserInfoPath:
p.UserInfo(rw, req) p.UserInfo(rw, req)
case strings.HasPrefix(path, fmt.Sprintf("%s/static", p.ProxyPrefix)):
p.ServeStatic(rw, req)
default: default:
p.Proxy(rw, req) p.Proxy(rw, req)
} }
} }
func (p *OAuthProxy) ServeStatic(rw http.ResponseWriter, req *http.Request) {
staticFs := http.FileServer(http.FS(staticWeb.StaticDist))
http.StripPrefix(fmt.Sprintf("%s/static", p.ProxyPrefix), staticFs).ServeHTTP(rw, req)
}
//UserInfo endpoint outputs session email and preferred username in JSON format //UserInfo endpoint outputs session email and preferred username in JSON format
func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) { func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {

View File

@ -4,25 +4,11 @@ import (
"html/template" "html/template"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"goauthentik.io/internal/outpost/proxy/templates"
) )
func getTemplates() *template.Template { func getTemplates() *template.Template {
t, err := template.New("foo").Parse(`{{define "error.html"}} t, err := template.New("foo").Parse(templates.ErrorTemplate)
<!DOCTYPE html>
<html lang="en" charset="utf-8">
<head>
<title>{{.Title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<style>* { font-family: sans-serif; }</style>
</head>
<body>
<h2>{{.Title}}</h2>
<p>{{.Message}}</p>
<hr>
<p><a href="{{.ProxyPrefix}}/sign_in">Sign In</a></p>
<p>Powered by <a href="https://goauthentik.io">authentik</a></p>
</body>
</html>{{end}}`)
if err != nil { if err != nil {
log.Fatalf("failed parsing template %s", err) log.Fatalf("failed parsing template %s", err)
} }

View File

@ -0,0 +1,65 @@
{{define "error.html"}}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{{.Title}}</title>
<link rel="shortcut icon" type="image/png" href="/akprox/static/dist/assets/icons/icon.png">
<link rel="stylesheet" type="text/css" href="/akprox/static/dist/patternfly.min.css">
<link rel="stylesheet" type="text/css" href="/akprox/static/dist/authentik.css">
<style>
.pf-c-background-image::before {
--ak-flow-background: url("/akprox/static/dist/assets/images/flow_background.jpg");
}
</style>
</head>
<body>
<div class="pf-c-background-image">
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
<filter id="image_overlay">
<feColorMatrix in="SourceGraphic" type="matrix" values="1.3 0 0 0 0 0 1.3 0 0 0 0 0 1.3 0 0 0 0 0 1 0" />
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
<feFuncA type="table" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</svg>
</div>
<div class="pf-c-login">
<div class="ak-login-container">
<header class="pf-c-login__header">
<div class="pf-c-brand ak-brand">
<img src="/akprox/static/dist/assets/icons/icon_left_brand.svg" alt="authentik icon" />
</div>
</header>
<main class="pf-c-login__main">
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
{{ .Title }}
</h1>
</header>
<div class="pf-c-login__main-body">
{{ .Message }}
</div>
<div class="pf-c-login__main-body">
<a href="/" class="pf-c-button pf-m-primary pf-m-block">Go to home</a>
</div>
</main>
<footer class="pf-c-login__footer">
<p></p>
<ul class="pf-c-list pf-m-inline">
<li>
<a href="https://goauthentik.io">
Powered by authentik
</a>
</li>
</ul>
</footer>
</div>
</div>
</body>
</html>
{{end}}

View File

@ -0,0 +1,6 @@
package templates
import _ "embed"
//go:embed error.html
var ErrorTemplate string

13
internal/utils/net.go Normal file
View File

@ -0,0 +1,13 @@
package utils
import "net"
func GetIP(addr net.Addr) string {
switch addr := addr.(type) {
case *net.UDPAddr:
return addr.IP.String()
case *net.TCPAddr:
return addr.IP.String()
}
return ""
}

View File

@ -9,19 +9,21 @@ import (
"goauthentik.io/internal/utils/web" "goauthentik.io/internal/utils/web"
) )
func loggingMiddleware(next http.Handler) http.Handler { func loggingMiddleware(l *log.Entry) func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return func(next http.Handler) http.Handler {
span := sentry.StartSpan(r.Context(), "authentik.go.request") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
before := time.Now() span := sentry.StartSpan(r.Context(), "authentik.go.request")
// Call the next handler, which can be another middleware in the chain, or the final handler. before := time.Now()
next.ServeHTTP(w, r) // Call the next handler, which can be another middleware in the chain, or the final handler.
after := time.Now() next.ServeHTTP(w, r)
log.WithFields(log.Fields{ after := time.Now()
"remote": r.RemoteAddr, l.WithFields(log.Fields{
"method": r.Method, "remote": r.RemoteAddr,
"took": after.Sub(before), "method": r.Method,
"host": web.GetHost(r), "took": after.Sub(before),
}).Info(r.RequestURI) "host": web.GetHost(r),
span.Finish() }).Info(r.RequestURI)
}) span.Finish()
})
}
} }

View File

@ -30,6 +30,7 @@ type WebServer struct {
} }
func NewWebServer() *WebServer { func NewWebServer() *WebServer {
l := log.WithField("logger", "authentik.g.web")
mainHandler := mux.NewRouter() mainHandler := mux.NewRouter()
if config.G.ErrorReporting.Enabled { if config.G.ErrorReporting.Enabled {
mainHandler.Use(recoveryMiddleware()) mainHandler.Use(recoveryMiddleware())
@ -37,14 +38,14 @@ func NewWebServer() *WebServer {
mainHandler.Use(handlers.ProxyHeaders) mainHandler.Use(handlers.ProxyHeaders)
mainHandler.Use(handlers.CompressHandler) mainHandler.Use(handlers.CompressHandler)
logginRouter := mainHandler.NewRoute().Subrouter() logginRouter := mainHandler.NewRoute().Subrouter()
logginRouter.Use(loggingMiddleware) logginRouter.Use(loggingMiddleware(l))
ws := &WebServer{ ws := &WebServer{
LegacyProxy: true, LegacyProxy: true,
m: mainHandler, m: mainHandler,
lh: logginRouter, lh: logginRouter,
log: log.WithField("logger", "authentik.g.web"), log: l,
} }
ws.configureStatic() ws.configureStatic()
ws.configureProxy() ws.configureProxy()

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