Compare commits

..

192 Commits

Author SHA1 Message Date
49dfb4756e release: 2022.1.4 2022-02-01 20:12:55 +01:00
814758e2aa website/docs: prepare 2022.1.4
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-02-01 19:27:25 +01:00
5c42dac5e2 web/user: include locale code in locale selection
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-02-01 19:19:37 +01:00
88603fa4f7 providers/proxy: set traefik labels using object_naming_template instead of UUID 2022-02-01 17:13:27 +00:00
0232c4e162 lifecycle: send analytics in gunicorn config to decrease outgoing requests when workers get restarted
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-02-01 15:01:43 +01:00
11753c1fe1 build(deps): bump django from 4.0.1 to 4.0.2 (#2204)
Bumps [django](https://github.com/django/django) from 4.0.1 to 4.0.2.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/4.0.1...4.0.2)

---
updated-dependencies:
- dependency-name: django
  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>
2022-02-01 10:59:04 +01:00
f5cc6c67ec providers/proxy: fix routing for external_host when using forward_auth_domain
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#2180
2022-02-01 10:14:46 +01:00
8b8ed3527a build(deps): bump @typescript-eslint/parser in /web (#2200) 2022-02-01 09:11:41 +01:00
1aa0274e7c build(deps): bump @typescript-eslint/eslint-plugin in /web (#2201) 2022-02-01 09:09:34 +01:00
ecd33ca0c1 build(deps): bump github.com/go-openapi/runtime from 0.21.1 to 0.22.0 (#2202) 2022-02-01 09:09:18 +01:00
e93be0de9a sources/ldap: add list_flatten function to property mappings, enable on managed LDAP mappings
closes #2199

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-31 23:07:32 +01:00
a5adc4f8ed core: fix view_token permission not being assigned on token creation for non-admin user
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-31 20:00:30 +01:00
a6baed9753 web/flows: fix width on flow container 2022-01-31 14:11:25 +00:00
ceaf832e63 root: remove boto integration in sentry to ease backup removal 2022-01-31 13:47:18 +00:00
a6b0b14685 Translate /web/src/locales/en.po in pl (#2197)
translation completed for the source file '/web/src/locales/en.po'
on the 'pl' language.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2022-01-31 14:17:26 +01:00
f679250edd lifecycle: remove gunicorn reload option
should help with #2159
2022-01-31 12:06:08 +00:00
acc4de2235 web: add pl locale 2022-01-31 11:50:05 +00:00
56a8276dbf website/integrations: update active directory docs (#2177) 2022-01-31 12:11:01 +01:00
6dfe6edbef website/integrations: add zulip (#2106)
* add zulip to sidebar links

* added Zulip chat integration documentation

* fix markdown typo

* add note about using Post for saml binding

* added missing ACS info and cleaned up

format matches other integration documents
2022-01-31 12:10:30 +01:00
6af4bd0d9a build(deps): bump construct-style-sheets-polyfill in /web (#2189)
Bumps [construct-style-sheets-polyfill](https://github.com/calebdwilliams/construct-style-sheets) from 3.0.5 to 3.1.0.
- [Release notes](https://github.com/calebdwilliams/construct-style-sheets/releases)
- [Changelog](https://github.com/calebdwilliams/construct-style-sheets/blob/main/CHANGELOG.md)
- [Commits](https://github.com/calebdwilliams/construct-style-sheets/commits)

---
updated-dependencies:
- dependency-name: construct-style-sheets-polyfill
  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>
2022-01-31 12:09:13 +01:00
7ee7f6bd6a Translate /web/src/locales/en.po in pl (#2196)
translation completed for the source file '/web/src/locales/en.po'
on the 'pl' language.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2022-01-31 10:00:37 +01:00
f8b8334010 build(deps): bump @patternfly/patternfly from 4.164.2 to 4.171.1 in /web (#2192) 2022-01-31 09:05:17 +01:00
d4b65dc4b4 build(deps): bump @sentry/browser from 6.17.2 to 6.17.3 in /web (#2191) 2022-01-31 09:04:40 +01:00
e4bbd3b1c0 build(deps): bump eslint from 8.7.0 to 8.8.0 in /web (#2190) 2022-01-31 09:03:47 +01:00
87de5e625d build(deps): bump @sentry/tracing from 6.17.2 to 6.17.3 in /web (#2193) 2022-01-31 09:03:32 +01:00
efbe51673e build(deps): bump pycryptodome from 3.13.0 to 3.14.0 (#2194) 2022-01-31 09:03:10 +01:00
a95bea53ea build(deps): bump github.com/prometheus/client_golang (#2195) 2022-01-31 09:02:56 +01:00
6021fc0f52 providers/proxy: fix backend override persisting for other users
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-30 22:29:34 +01:00
1415b68ff4 web: add es locale
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-30 21:43:55 +01:00
be6853ac52 Translate /web/src/locales/en.po in es (#2188)
translation completed for the source file '/web/src/locales/en.po'
on the 'es' language.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2022-01-30 21:38:30 +01:00
7fd6be5abb providers/proxy: add backend_override
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-30 21:35:08 +01:00
91d6f572a5 scripts: cleanup
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-30 21:34:37 +01:00
016a9ce34e build(deps): bump boto3 from 1.20.45 to 1.20.46 (#2187) 2022-01-30 00:52:25 +01:00
8adb95af7f build(deps): bump uvicorn from 0.17.0.post1 to 0.17.1 (#2186) 2022-01-30 00:52:08 +01:00
1dc54775d8 build(deps): bump requests-oauthlib from 1.3.0 to 1.3.1 (#2185) 2022-01-30 00:51:59 +01:00
370ef716b5 build(deps-dev): bump black from 21.12b0 to 22.1.0 (#2184) 2022-01-30 00:51:49 +01:00
16e56ad9ca website/docs: add rough documentation style guide
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-29 23:52:03 +01:00
b5b5a9eed3 web/admin: only check first half of locale when detecting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#2178
2022-01-28 12:35:37 +01:00
8b22e7bcc3 core: compile backend translations (#2179) 2022-01-28 11:09:29 +01:00
d48b5b9511 Translate /locale/en/LC_MESSAGES/django.po in es (#2175)
translation completed for the source file '/locale/en/LC_MESSAGES/django.po'
on the 'es' language.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2022-01-28 09:55:56 +01:00
0eccaa3f1e build(deps): bump boto3 from 1.20.44 to 1.20.45 (#2176)
Bumps [boto3](https://github.com/boto/boto3) from 1.20.44 to 1.20.45.
- [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.20.44...1.20.45)

---
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>
2022-01-28 09:55:16 +01:00
67d550a80d providers/proxy: don't include hostname and scheme in redirect when we only got a path and not a full URL
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-27 18:23:08 +01:00
ebb5711c32 providers/proxy: add support for X-Original-URI in nginx, better handle missing headers and report errors to authentik
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-27 18:14:02 +01:00
79ec872232 build(deps): bump @docusaurus/plugin-client-redirects in /website (#2173)
Bumps [@docusaurus/plugin-client-redirects](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-plugin-client-redirects) from 2.0.0-beta.14 to 2.0.0-beta.15.
- [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.15/packages/docusaurus-plugin-client-redirects)

---
updated-dependencies:
- dependency-name: "@docusaurus/plugin-client-redirects"
  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>
2022-01-27 10:14:14 +01:00
4284e14ff7 build(deps): bump @docusaurus/preset-classic in /website (#2172)
Bumps [@docusaurus/preset-classic](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-preset-classic) from 2.0.0-beta.14 to 2.0.0-beta.15.
- [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.15/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>
2022-01-27 10:12:06 +01:00
92a09779d0 build(deps): bump boto3 from 1.20.43 to 1.20.44 (#2174) 2022-01-27 09:28:02 +01:00
14c621631d web: Update Web API Client version (#2170)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-01-26 23:30:56 +01:00
c55f503b9b release: 2022.1.3 2022-01-26 22:15:28 +01:00
a908cad976 website/docs: add release notes for 2022.1.3
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-26 21:41:15 +01:00
c2586557d8 root: fix redis passwords not being encoded correctly
closes #2130

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-26 20:45:45 +01:00
01c80a82e2 web/admin: fix SMS Stage form not working
closes #2127

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-26 20:39:38 +01:00
0d47654651 root: add max-requests for gunicorn and max tasks for celery
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-26 10:04:58 +01:00
1183095833 build(deps): bump @sentry/tracing from 6.17.1 to 6.17.2 in /web (#2162) 2022-01-26 09:35:10 +01:00
c281b11bdc build(deps): bump lit from 2.1.1 to 2.1.2 in /web (#2161) 2022-01-26 09:22:05 +01:00
61fe45a58c build(deps): bump @sentry/browser from 6.17.1 to 6.17.2 in /web (#2163) 2022-01-26 09:21:52 +01:00
d43aab479c build(deps): bump rollup from 2.66.0 to 2.66.1 in /web (#2164) 2022-01-26 09:21:43 +01:00
7f8383427a build(deps): bump sentry-sdk from 1.5.3 to 1.5.4 (#2165) 2022-01-26 09:21:24 +01:00
a06d6cf33d build(deps-dev): bump bandit from 1.7.1 to 1.7.2 (#2166) 2022-01-26 09:21:09 +01:00
5b7cb205c9 build(deps): bump boto3 from 1.20.42 to 1.20.43 (#2167) 2022-01-26 09:20:50 +01:00
293a932d20 build(deps-dev): bump coverage from 6.2 to 6.3 (#2168) 2022-01-26 09:20:34 +01:00
fff901ff03 rootL Fix goauthentik.io URL in Readme (#2158) 2022-01-25 20:36:44 +01:00
f47c936295 internal: add optional debug server listening on 9900
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-25 17:18:53 +01:00
88d5aec618 web/admin: fix links which look like labels
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-25 16:13:30 +01:00
96ae68cf09 internal: make error message less confusing
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-25 15:45:21 +01:00
63b3434b6f website/docs: improve nginx examples
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-25 14:25:21 +01:00
947ecec02b outposts/ldap: Fix more case sensitivity issues. (#2144) 2022-01-25 11:27:27 +01:00
1c2b452406 outposts/proxy: fix potential empty redirect, add tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#2141
2022-01-25 10:57:53 +01:00
47777529ac build(deps): bump @formatjs/intl-listformat from 6.5.0 to 6.5.1 in /web (#2154) 2022-01-25 09:50:29 +01:00
949095c376 build(deps): bump @lingui/macro from 3.13.1 to 3.13.2 in /web (#2152) 2022-01-25 09:49:59 +01:00
4b112c2799 build(deps): bump @sentry/browser from 6.16.1 to 6.17.1 in /web (#2146) 2022-01-25 09:49:48 +01:00
291a2516b1 build(deps): bump @typescript-eslint/eslint-plugin in /web (#2149) 2022-01-25 09:49:29 +01:00
4dcfd021e2 build(deps): bump @lingui/detect-locale from 3.13.1 to 3.13.2 in /web (#2147) 2022-01-25 09:49:13 +01:00
ca50848db3 build(deps): bump boto3 from 1.20.41 to 1.20.42 (#2156) 2022-01-25 09:49:01 +01:00
0bb3e3c558 build(deps): bump @lingui/cli from 3.13.1 to 3.13.2 in /web (#2148) 2022-01-25 09:48:50 +01:00
e4b25809ab build(deps): bump @typescript-eslint/parser in /web (#2150) 2022-01-25 09:48:03 +01:00
7bf932f8e2 build(deps): bump @sentry/tracing from 6.16.1 to 6.17.1 in /web (#2151) 2022-01-25 09:47:52 +01:00
99d04528b0 build(deps): bump country-flag-icons from 1.4.19 to 1.4.20 in /web (#2153) 2022-01-25 09:47:43 +01:00
e48d172036 build(deps): bump @lingui/core from 3.13.1 to 3.13.2 in /web (#2155) 2022-01-25 09:47:34 +01:00
c2388137a8 build(deps): bump uvicorn from 0.17.0 to 0.17.0.post1 (#2157) 2022-01-25 09:47:05 +01:00
650e2cbc38 internal: remove duplicate log messages
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-24 22:25:35 +01:00
b32800ea71 outposts/proxy: trace full headers to debug
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-24 22:08:31 +01:00
e1c0c0b20c internal: don't override server header
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-24 22:05:11 +01:00
fe39e39dcd lifecycle: make secret_key warning more prominent
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#2131
2022-01-24 21:52:16 +01:00
883f213b03 lifecycle: wait for db in worker
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-24 21:51:57 +01:00
538996f617 web: Update Web API Client version (#2143)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-01-24 21:46:39 +01:00
2f4c92deb9 Merge branch 'version-2022.1' 2022-01-24 21:42:12 +01:00
ef335ec083 outposts/proxy: add more test cases for domain-level auth
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-24 21:41:15 +01:00
07b09df3fe internal: add more outpost tests, add support for X-Original-URL
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-24 20:50:13 +01:00
e70e031a1f internal: start adding tests to outpost
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-24 20:12:25 +01:00
c7ba183dc0 providers/proxy: fix traefik label
closes #2128

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-24 17:45:09 +01:00
3ed23a37ea website/docs: add 2022.1.2 release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-24 11:34:13 +01:00
3d724db0e3 release: 2022.1.2 2022-01-24 11:28:00 +01:00
2997542114 lib: disable backup by default, add note to configuration 2022-01-24 10:00:15 +00:00
84b18fff96 ci: cache-v2
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-24 09:37:04 +01:00
1dce408c72 internal/proxyv2: only allow access to /akprox in nginx mode when forward url could be extracted
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-24 09:30:33 +01:00
e5ff47bf14 build(deps): bump @lingui/cli from 3.13.0 to 3.13.1 in /web (#2133) 2022-01-24 08:49:03 +01:00
b53bf331c3 build(deps): bump @lingui/macro from 3.13.0 to 3.13.1 in /web (#2135) 2022-01-24 08:48:51 +01:00
90e9a8b34c build(deps): bump rollup from 2.64.0 to 2.66.0 in /web (#2139) 2022-01-24 08:48:26 +01:00
845f842783 build(deps): bump @lingui/core from 3.13.0 to 3.13.1 in /web (#2136) 2022-01-24 08:48:17 +01:00
7397849c60 build(deps): bump rapidoc from 9.1.3 to 9.1.4 in /website (#2132) 2022-01-24 08:47:45 +01:00
6dd46b5fc5 build(deps): bump @babel/core from 7.16.10 to 7.16.12 in /web (#2134) 2022-01-24 08:47:35 +01:00
89ca79ed10 build(deps): bump @lingui/detect-locale from 3.13.0 to 3.13.1 in /web (#2137) 2022-01-24 08:47:15 +01:00
713bef895c build(deps): bump rapidoc from 9.1.3 to 9.1.4 in /web (#2138) 2022-01-24 08:46:37 +01:00
925115e9ce build(deps): bump github.com/go-openapi/runtime from 0.21.0 to 0.21.1 (#2140) 2022-01-24 08:46:17 +01:00
42f5cf8c93 outposts: allow custom label for docker containers
closes #2128

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-23 21:55:58 +01:00
82cc1d536a providers/proxy: add PathPrefix to auto-traefik labels
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#2128
2022-01-23 21:55:46 +01:00
08af2fd46b website/docs: deprecate inbuilt backup
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-23 21:51:22 +01:00
70e3b27a4d root: upgrade python dependencies
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-23 21:27:16 +01:00
6a411d7960 policies/hibp: ensure password is encodable
closes AUTHENTIK-1SA

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-23 21:23:24 +01:00
33567b56d7 lifecycle: replace lowercase, deprecated prometheus_multiproc_dir
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-23 21:21:06 +01:00
0c1954aeb7 web: Update Web API Client version (#2126)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-01-22 19:06:20 +01:00
f4a6c70e98 release: 2022.1.1 2022-01-22 18:28:40 +01:00
5f198e7fe4 website/docs: update 2022.1
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-22 12:29:20 +01:00
d172d32817 ci: bump golangci
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-21 23:22:59 +01:00
af3fb5c2cd internal: use math.MaxInt for compatibility
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1819
2022-01-21 23:11:17 +01:00
885efb526e web/admin: also set embedded outpost host when it doesn't include scheme
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-21 13:51:34 +01:00
3bfb8b2cb2 outposts/proxyv2: allow access to /akprox urls in forward auth mode to make routing in nginx/traefik easier
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-21 13:43:16 +01:00
9fc5ff4b77 outposts/proxyv2: fix JWKS url pointing to localhost on embedded outpost
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-21 13:29:51 +01:00
dd8b579dd6 lib: ignore paramiko logger
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-21 10:46:33 +01:00
e12cbd8711 website/docs: add 2022.1
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-21 09:47:49 +01:00
62d35f8f8c build(deps): bump codemirror from 5.65.0 to 5.65.1 in /web (#2122) 2022-01-21 09:06:27 +01:00
49be504c13 build(deps): bump @babel/preset-env from 7.16.10 to 7.16.11 in /web (#2123) 2022-01-21 09:06:04 +01:00
edad55e51d build(deps): bump typescript from 4.5.4 to 4.5.5 in /web (#2124) 2022-01-21 09:05:51 +01:00
38086fa8bb build(deps): bump boto3 from 1.20.39 to 1.20.40 (#2125) 2022-01-21 09:05:32 +01:00
c4f9a3e9a7 build(deps): bump @babel/preset-env from 7.16.8 to 7.16.10 in /web (#2114) 2022-01-20 08:45:49 +01:00
930df791bd build(deps): bump python (#2113) 2022-01-20 08:45:28 +01:00
9a6086634c build(deps): bump boto3 from 1.20.38 to 1.20.39 (#2117) 2022-01-20 08:45:03 +01:00
b68e65355a build(deps): bump @babel/core from 7.16.7 to 7.16.10 in /web (#2115) 2022-01-20 08:44:47 +01:00
72d33a91dd build(deps): bump @babel/plugin-transform-runtime in /web (#2116) 2022-01-20 08:44:28 +01:00
7067e3d69a build(deps): bump github.com/prometheus/client_golang (#2118) 2022-01-20 08:44:01 +01:00
4db370d24e website/docs: add flow inspector docs
closes #2105

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-19 19:31:21 +01:00
41e7b9b73f outposts/proxyv2: fix before-redirect url not being saved in proxy mode
closes #2109

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-19 19:16:30 +01:00
7f47f93e4e internal: cleanup log messages
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-19 19:01:24 +01:00
89abd44b76 lifecycle: add early check for missing/invalid secret key
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-19 09:53:53 +01:00
14c7d8c4f4 internal: route traffic to proxy providers based on cookie domain when multiple domain-level providers exist
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#2079
2022-01-18 23:19:43 +01:00
525976a81b root: upgrade python dependencies
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-18 21:35:07 +01:00
64a2126ea4 website/docs: fix typo in port mapping in manifest (#2103)
Otherwise it causes:

```
error: error validating "outpost.yaml": error validating data: ValidationError(Service.spec.ports[1].port): invalid type for io.k8s.api.core.v1.ServicePort.port: got "string", expected "integer"; if you choose to ignore these errors, turn validation off with --validate=false
```
2022-01-18 19:57:55 +01:00
994c5882ab root: fix error if secret_key is purely numerical
closes #2099

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-18 09:17:33 +01:00
ad64d51e85 build(deps): bump @typescript-eslint/eslint-plugin in /web (#2100)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.9.1 to 5.10.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.10.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  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>
2022-01-18 09:16:54 +01:00
a184a7518a build(deps): bump @typescript-eslint/parser from 5.9.1 to 5.10.0 in /web (#2101)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.9.1 to 5.10.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.10.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-18 09:14:58 +01:00
943fd80920 web: ignore additional error
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-17 22:05:23 +01:00
01bb18b8c4 root: allow customisation of ports in compose without override
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-17 14:48:02 +01:00
94baaaa5a5 build(deps): bump eslint from 8.6.0 to 8.7.0 in /web (#2096) 2022-01-17 09:19:59 +01:00
40b164ce94 build(deps): bump rollup from 2.63.0 to 2.64.0 in /web (#2097) 2022-01-17 09:18:30 +01:00
1d7c7801e7 website/docs: prepare 2022.1 release
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-16 18:22:25 +01:00
0db0a12ef3 root: rename csrf header
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-16 16:17:44 +01:00
8008aba450 web: directly read csrf token before injecting into request
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-16 16:10:55 +01:00
eaeab27004 lib: add support for custom env
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-16 14:56:02 +01:00
111fbf119b *: refactor prometheus gauges to directly updating metrics view
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-16 13:57:07 +01:00
300ad88447 web: add polyfill for Intl.ListFormat
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-15 14:56:18 +01:00
92cc0c9c64 root: decrease to 10 backup history
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-14 19:59:50 +01:00
18ff803370 outposts: trigger service update on k8s when selector doesnt match
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-14 11:42:57 +01:00
819af78e2b internal: make internal go version match python version
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-14 10:45:37 +01:00
6338785ce1 outposts: change label app.kubernetes.io/name to include outpost type
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-14 10:34:54 +01:00
973e151dff outposts: add Additional version labels to managed k8s deployments
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-13 17:48:01 +01:00
fae6d83f27 *: simplify extracting current version info
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-13 17:47:31 +01:00
ed84fe0b8d root: set samesite for csrf cookie
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-12 23:14:14 +01:00
1ee603403e root: upgrade python dependencies
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-12 23:02:39 +01:00
7db7b7cc4d stages/authenticator_validate: fix lint
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-12 23:00:28 +01:00
68a98cd86c web: Update Web API Client version (#2091)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-01-12 22:59:59 +01:00
e758db5727 stages/authenticator_webauthn: make more WebAuthn options configurable
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-12 22:57:49 +01:00
4d7d700afa providers/oauth2: change default redirect uri behaviour; set first used url when blank and use star for wildcard
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-12 22:44:57 +01:00
f9a5add01d root: include build in analytics
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-12 22:18:52 +01:00
2986b56389 root: fix backups running every minute instead of once
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-12 22:09:44 +01:00
58f79b525d web/admin: fix invalid build due to wrong import
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-11 11:40:59 +01:00
0a1c0dae05 build(deps): bump @typescript-eslint/eslint-plugin in /web (#2085) 2022-01-11 08:59:13 +01:00
e18ef8dab6 build(deps): bump @babel/preset-env from 7.16.7 to 7.16.8 in /web (#2087) 2022-01-11 08:58:45 +01:00
3cacc59bec build(deps): bump @typescript-eslint/parser from 5.9.0 to 5.9.1 in /web (#2086) 2022-01-11 08:57:51 +01:00
4eea46d399 build(deps): bump @babel/plugin-transform-runtime in /web (#2088) 2022-01-11 08:57:28 +01:00
11e25617bd crypto: fully parse certificate on validation in serializer to prevent invalid certificates from being saved
closes #2082

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-10 20:36:50 +01:00
4817126811 website/integrations: fix synapse docs based on upstream docs
closes #2080

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-10 20:19:56 +01:00
0181361efa website/integrations: use Signing Key instead of RSA Key
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-10 20:18:05 +01:00
8ff8e1d5f7 web/admin: fix missing configure flow setting on webuahtn setup stage form
closes #2084

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-10 20:15:34 +01:00
19d5902a92 flows: handle error if flow title contains invalid format string
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-10 19:49:27 +01:00
71dffb21a9 outposts: improve error handling for outpost service connection state
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-10 19:44:13 +01:00
bd283c506d web/flows: remove node directly instead of using removeChild()
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-10 19:37:51 +01:00
ef564e5f1a web: fix double plural in label
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-10 18:58:56 +01:00
2543224c7c core: dont return 404 when trying to view key of expired token
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-10 17:53:09 +01:00
077eee9310 Revert "build(deps): bump goauthentik.io/api from 0.2021125.1 to 1.2021.9 (#2083)"
This reverts commit d894eeaa67.
2022-01-10 10:03:48 +01:00
d894eeaa67 build(deps): bump goauthentik.io/api from 0.2021125.1 to 1.2021.9 (#2083)
Bumps [goauthentik.io/api](https://github.com/goauthentik/client-go) from 0.2021125.1 to 1.2021.9.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/commits)

---
updated-dependencies:
- dependency-name: goauthentik.io/api
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-10 09:39:30 +01:00
452bfb39bf Revert "web/elements: re-enable codemirror line numbers (fixed on firefox)"
This reverts commit 4c166dcf52.

closes #2081

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-09 22:24:32 +01:00
6b6702521f api: don't return error reporting enabled when debug is enabled
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-07 21:53:22 +01:00
c07b8d95d0 outposts/proxy: remove deprecated headers
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-07 17:01:23 +01:00
bf347730b3 outposts/ldap: remove deprecated fields
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-07 09:52:19 +01:00
ececfc3a30 internal: fix comment formatting for TODOs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-07 09:51:41 +01:00
b76546de0c Translate /web/src/locales/en.po in tr (#2074) 2022-01-07 09:16:02 +01:00
424d490a60 build(deps): bump golang from 1.17.5-bullseye to 1.17.6-bullseye (#2075) 2022-01-07 09:14:44 +01:00
127dd85214 build(deps): bump lit from 2.1.0 to 2.1.1 in /web (#2076) 2022-01-07 09:14:23 +01:00
10570ac7f8 build(deps): bump goauthentik.io/api from 0.2021124.9 to 0.2021125.1 (#2077) 2022-01-07 09:14:07 +01:00
dc5667b0b8 web: Update Web API Client version (#2073)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2022-01-06 22:23:23 +01:00
ec9cacb610 ci: post-release cleanup
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-06 22:22:50 +01:00
0027dbc0e5 root: remove old api path
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-01-06 22:21:21 +01:00
163 changed files with 19269 additions and 3079 deletions

View File

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

View File

@ -40,7 +40,7 @@ jobs:
uses: actions/cache@v2.1.7
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
@ -56,7 +56,7 @@ jobs:
uses: actions/cache@v2.1.7
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
@ -79,7 +79,7 @@ jobs:
uses: actions/cache@v2.1.7
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
- name: checkout stable
run: |
# Copy current, latest config to local
@ -95,11 +95,7 @@ jobs:
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
run: |
scripts/ci_prepare.sh
# Sync anyways since stable will have different dependencies
# TODO: Remove after next stable release
if [[ -f "Pipfile.lock" ]]; then
pipenv install --dev
fi
# install anyways since stable will have different dependencies
poetry install
- name: run migrations to stable
run: poetry run python -m lifecycle.migrate
@ -108,13 +104,7 @@ jobs:
set -x
git fetch
git reset --hard HEAD
# TODO: Remove after next stable release
rm -f poetry.lock
git checkout $GITHUB_SHA
# TODO: Remove after next stable release
if [[ -f "Pipfile.lock" ]]; then
pipenv install --dev
fi
poetry install
- name: prepare
env:
@ -131,7 +121,7 @@ jobs:
uses: actions/cache@v2.1.7
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
@ -158,7 +148,7 @@ jobs:
uses: actions/cache@v2.1.7
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
@ -195,7 +185,7 @@ jobs:
uses: actions/cache@v2.1.7
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
@ -240,7 +230,7 @@ jobs:
uses: actions/cache@v2.1.7
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}

View File

@ -28,11 +28,27 @@ jobs:
--rm \
-v $(pwd):/app \
-w /app \
golangci/golangci-lint:v1.39.0 \
golangci/golangci-lint:v1.43 \
golangci-lint run -v --timeout 200s
test-unittest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: "^1.17"
- name: Get dependencies
run: |
go get github.com/axw/gocov/gocov
go get github.com/AlekSi/gocov-xml
go get github.com/jstemmer/go-junit-report
- name: Go unittests
run: |
go test -timeout 0 -v -race -coverprofile=coverage.out -covermode=atomic -cover ./... | go-junit-report > junit.xml
ci-outpost-mark:
needs:
- lint-golint
- test-unittest
runs-on: ubuntu-latest
steps:
- run: echo mark

View File

@ -30,14 +30,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik:2021.12.5,
beryju/authentik:2022.1.4,
beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.12.5,
ghcr.io/goauthentik/server:2022.1.4,
ghcr.io/goauthentik/server:latest
platforms: linux/amd64,linux/arm64
context: .
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.12.5', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2022.1.4', 'rc') }}
run: |
docker pull beryju/authentik:latest
docker tag beryju/authentik:latest beryju/authentik:stable
@ -78,14 +78,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-${{ matrix.type }}:2021.12.5,
beryju/authentik-${{ matrix.type }}:2022.1.4,
beryju/authentik-${{ matrix.type }}:latest,
ghcr.io/goauthentik/${{ matrix.type }}:2021.12.5,
ghcr.io/goauthentik/${{ matrix.type }}:2022.1.4,
ghcr.io/goauthentik/${{ matrix.type }}:latest
file: ${{ matrix.type }}.Dockerfile
platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.12.5', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2022.1.4', 'rc') }}
run: |
docker pull beryju/authentik-${{ matrix.type }}:latest
docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable
@ -170,7 +170,7 @@ jobs:
SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org
with:
version: authentik@2021.12.5
version: authentik@2022.1.4
environment: beryjuorg-prod
sourcemaps: './web/dist'
url_prefix: '~/static/dist'

View File

@ -26,7 +26,7 @@ jobs:
uses: actions/cache@v2.1.7
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}

View File

@ -1 +0,0 @@
3.9.7

View File

@ -16,7 +16,7 @@ ENV NODE_ENV=production
RUN cd /work/web && npm i && npm run build
# Stage 3: Build go proxy
FROM docker.io/golang:1.17.5-bullseye AS builder
FROM docker.io/golang:1.17.6-bullseye AS builder
WORKDIR /work
@ -32,7 +32,7 @@ COPY ./go.sum /work/go.sum
RUN go build -o /work/authentik ./cmd/server/main.go
# Stage 4: Run
FROM docker.io/python:3.10.1-slim-bullseye
FROM docker.io/python:3.10.2-slim-bullseye
LABEL org.opencontainers.image.url https://goauthentik.io
LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info.

View File

@ -15,6 +15,9 @@ test-e2e-provider:
test-e2e-rest:
coverage run manage.py test tests/e2e/test_flows* tests/e2e/test_source*
test-go:
go test -timeout 0 -v -race -cover ./...
test:
coverage run manage.py test authentik
coverage html

View File

@ -57,4 +57,4 @@ DigitalOcean provides development and testing resources for authentik.
</a>
</p>
Netlify hosts the [goauthentik.io](goauthentik.io) site.
Netlify hosts the [goauthentik.io](https://goauthentik.io) site.

View File

@ -1,3 +1,19 @@
"""authentik"""
__version__ = "2021.12.5"
from os import environ
from typing import Optional
__version__ = "2022.1.4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
def get_build_hash(fallback: Optional[str] = None) -> str:
"""Get build hash"""
return environ.get(ENV_GIT_HASH_KEY, fallback if fallback else "")
def get_full_version() -> str:
"""Get full version, with build hash appended"""
version = __version__
if (build_hash := get_build_hash()) != "":
version += "." + build_hash
return version

View File

@ -1,6 +1,4 @@
"""authentik administration overview"""
from os import environ
from django.core.cache import cache
from drf_spectacular.utils import extend_schema
from packaging.version import parse
@ -10,7 +8,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from authentik import ENV_GIT_HASH_KEY, __version__
from authentik import __version__, get_build_hash
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
from authentik.core.api.utils import PassiveSerializer
@ -25,7 +23,7 @@ class VersionSerializer(PassiveSerializer):
def get_build_hash(self, _) -> str:
"""Get build hash, if version is not latest or released"""
return environ.get(ENV_GIT_HASH_KEY, "")
return get_build_hash()
def get_version_current(self, _) -> str:
"""Get current version"""

View File

@ -1,4 +1,6 @@
"""authentik admin app config"""
from importlib import import_module
from django.apps import AppConfig
@ -13,3 +15,4 @@ class AuthentikAdminConfig(AppConfig):
from authentik.admin.tasks import clear_update_notifications
clear_update_notifications.delay()
import_module("authentik.admin.signals")

View File

@ -0,0 +1,23 @@
"""admin signals"""
from django.dispatch import receiver
from authentik.admin.api.tasks import TaskInfo
from authentik.admin.api.workers import GAUGE_WORKERS
from authentik.root.celery import CELERY_APP
from authentik.root.monitoring import monitoring_set
@receiver(monitoring_set)
# pylint: disable=unused-argument
def monitoring_set_workers(sender, **kwargs):
"""Set worker gauge"""
count = len(CELERY_APP.control.ping(timeout=0.5))
GAUGE_WORKERS.set(count)
@receiver(monitoring_set)
# pylint: disable=unused-argument
def monitoring_set_tasks(sender, **kwargs):
"""Set task gauges"""
for task in TaskInfo.all().values():
task.set_prom_metrics()

View File

@ -1,6 +1,5 @@
"""authentik admin tasks"""
import re
from os import environ
from django.core.cache import cache
from django.core.validators import URLValidator
@ -9,7 +8,7 @@ from prometheus_client import Info
from requests import RequestException
from structlog.stdlib import get_logger
from authentik import ENV_GIT_HASH_KEY, __version__
from authentik import __version__, get_build_hash
from authentik.events.models import Event, EventAction, Notification
from authentik.events.monitored_tasks import (
MonitoredTask,
@ -36,7 +35,7 @@ def _set_prom_info():
{
"version": __version__,
"latest": cache.get(VERSION_CACHE_KEY, ""),
"build_hash": environ.get(ENV_GIT_HASH_KEY, ""),
"build_hash": get_build_hash(),
}
)

View File

@ -30,7 +30,7 @@ function getCookie(name) {
window.addEventListener('DOMContentLoaded', (event) => {
const rapidocEl = document.querySelector('rapi-doc');
rapidocEl.addEventListener('before-try', (e) => {
e.detail.request.headers.append('X-CSRFToken', getCookie("authentik_csrf"));
e.detail.request.headers.append('X-authentik-CSRF', getCookie("authentik_csrf"));
});
});
</script>

View File

@ -4,7 +4,5 @@ from django.urls import include, path
from authentik.api.v3.urls import urlpatterns as v3_urls
urlpatterns = [
# TODO: Remove in 2022.1
path("v2beta/", include(v3_urls)),
path("v3/", include(v3_urls)),
]

View File

@ -80,7 +80,7 @@ class ConfigView(APIView):
config = ConfigSerializer(
{
"error_reporting": {
"enabled": CONFIG.y("error_reporting.enabled"),
"enabled": CONFIG.y("error_reporting.enabled") and not settings.DEBUG,
"environment": CONFIG.y("error_reporting.environment"),
"send_pii": CONFIG.y("error_reporting.send_pii"),
"traces_sample_rate": float(CONFIG.y("error_reporting.sample_rate", 0.4)),

View File

@ -1,10 +1,9 @@
"""Tokens API Viewset"""
from typing import Any
from django.http.response import Http404
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiResponse, extend_schema
from guardian.shortcuts import get_anonymous_user
from guardian.shortcuts import assign_perm, get_anonymous_user
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField
@ -96,10 +95,12 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
def perform_create(self, serializer: TokenSerializer):
if not self.request.user.is_superuser:
return serializer.save(
instance = serializer.save(
user=self.request.user,
expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True),
)
assign_perm("authentik_core.view_token_key", self.request.user, instance)
return instance
return super().perform_create(serializer)
@permission_required("authentik_core.view_token_key")
@ -114,7 +115,5 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
def view_key(self, request: Request, identifier: str) -> Response:
"""Return token key and log access"""
token: Token = self.get_object()
if token.is_expired:
raise Http404
Event.new(EventAction.SECRET_VIEW, secret=token).from_http(request) # noqa # nosec
return Response(TokenViewSerializer({"key": token.key}).data)

View File

@ -1,6 +1,7 @@
"""authentik core signals"""
from typing import TYPE_CHECKING
from django.apps import apps
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
@ -11,6 +12,8 @@ from django.dispatch import receiver
from django.http.request import HttpRequest
from prometheus_client import Gauge
from authentik.root.monitoring import monitoring_set
# Arguments: user: User, password: str
password_changed = Signal()
@ -20,6 +23,17 @@ if TYPE_CHECKING:
from authentik.core.models import AuthenticatedSession, User
@receiver(monitoring_set)
# pylint: disable=unused-argument
def monitoring_set_models(sender, **kwargs):
"""set models gauges"""
for model in apps.get_models():
GAUGE_MODELS.labels(
model_name=model._meta.model_name,
app=model._meta.app_label,
).set(model.objects.count())
@receiver(post_save)
# pylint: disable=unused-argument
def post_save_application(sender: type[Model], instance, created: bool, **_):
@ -27,11 +41,6 @@ def post_save_application(sender: type[Model], instance, created: bool, **_):
from authentik.core.api.applications import user_app_cache_key
from authentik.core.models import Application
GAUGE_MODELS.labels(
model_name=sender._meta.model_name,
app=sender._meta.app_label,
).set(sender.objects.count())
if sender != Application:
return
if not created: # pragma: no cover

View File

@ -30,6 +30,7 @@ class TestTokenAPI(APITestCase):
self.assertEqual(token.user, self.user)
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.expiring, True)
self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token))
def test_token_create_invalid(self):
"""Test token creation endpoint (invalid data)"""

View File

@ -17,6 +17,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
@ -26,6 +27,8 @@ from authentik.crypto.managed import MANAGED_KEY
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
LOGGER = get_logger()
class CertificateKeyPairSerializer(ModelSerializer):
"""CertificateKeyPair Serializer"""
@ -76,8 +79,11 @@ class CertificateKeyPairSerializer(ModelSerializer):
def validate_certificate_data(self, value: str) -> str:
"""Verify that input is a valid PEM x509 Certificate"""
try:
load_pem_x509_certificate(value.encode("utf-8"), default_backend())
except ValueError:
# Cast to string to fully load and parse certificate
# Prevents issues like https://github.com/goauthentik/authentik/issues/2082
str(load_pem_x509_certificate(value.encode("utf-8"), default_backend()))
except ValueError as exc:
LOGGER.warning("Failed to load certificate", exc=exc)
raise ValidationError("Unable to load certificate.")
return value
@ -86,12 +92,17 @@ class CertificateKeyPairSerializer(ModelSerializer):
# Since this field is optional, data can be empty.
if value != "":
try:
load_pem_private_key(
str.encode("\n".join([x.strip() for x in value.split("\n")])),
password=None,
backend=default_backend(),
# Cast to string to fully load and parse certificate
# Prevents issues like https://github.com/goauthentik/authentik/issues/2082
str(
load_pem_private_key(
str.encode("\n".join([x.strip() for x in value.split("\n")])),
password=None,
backend=default_backend(),
)
)
except (ValueError, TypeError):
except (ValueError, TypeError) as exc:
LOGGER.warning("Failed to load private key", exc=exc)
raise ValidationError("Unable to load private key (possibly encrypted?).")
return value

View File

@ -4,7 +4,7 @@ from typing import Any, Optional
from django.core.cache import cache
from django.http import HttpRequest
from prometheus_client import Histogram
from prometheus_client import Gauge, Histogram
from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span
from structlog.stdlib import BoundLogger, get_logger
@ -16,7 +16,6 @@ from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import Flow, FlowStageBinding, Stage
from authentik.lib.config import CONFIG
from authentik.policies.engine import PolicyEngine
from authentik.root.monitoring import UpdatingGauge
LOGGER = get_logger()
PLAN_CONTEXT_PENDING_USER = "pending_user"
@ -27,10 +26,9 @@ PLAN_CONTEXT_SOURCE = "source"
# Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan
# was restored.
PLAN_CONTEXT_IS_RESTORED = "is_restored"
GAUGE_FLOWS_CACHED = UpdatingGauge(
GAUGE_FLOWS_CACHED = Gauge(
"authentik_flows_cached",
"Cached flows",
update_func=lambda: len(cache.keys("flow_*") or []),
)
HIST_FLOWS_PLAN_TIME = Histogram(
"authentik_flows_plan_time",
@ -171,7 +169,6 @@ class FlowPlanner:
)
plan = self._build_plan(user, request, default_context)
cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
GAUGE_FLOWS_CACHED.update()
if not plan.bindings and not self.allow_empty_flows:
raise EmptyFlowException()
return plan

View File

@ -4,6 +4,9 @@ from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from structlog.stdlib import get_logger
from authentik.flows.planner import GAUGE_FLOWS_CACHED
from authentik.root.monitoring import monitoring_set
LOGGER = get_logger()
@ -14,6 +17,13 @@ def delete_cache_prefix(prefix: str) -> int:
return len(keys)
@receiver(monitoring_set)
# pylint: disable=unused-argument
def monitoring_set_flows(sender, **kwargs):
"""set flow gauges"""
GAUGE_FLOWS_CACHED.set(len(cache.keys("flow_*") or []))
@receiver(post_save)
@receiver(pre_delete)
# pylint: disable=unused-argument

View File

@ -118,9 +118,12 @@ class ChallengeStageView(StageView):
"""Allow usage of placeholder in flow title."""
if not self.executor.plan:
return self.executor.flow.title
return self.executor.flow.title % {
"app": self.executor.plan.context.get(PLAN_CONTEXT_APPLICATION, "")
}
try:
return self.executor.flow.title % {
"app": self.executor.plan.context.get(PLAN_CONTEXT_APPLICATION, "")
}
except ValueError:
return self.executor.flow.title
def _get_challenge(self, *args, **kwargs) -> Challenge:
with Hub.current.start_span(

View File

@ -6,7 +6,7 @@ postgresql:
port: 5432
password: 'env://POSTGRES_PASSWORD'
backup:
enabled: true
enabled: false
s3_backup:
access_key: ""
secret_key: ""

View File

@ -32,6 +32,7 @@ class BaseEvaluator:
self._globals = {
"regex_match": BaseEvaluator.expr_regex_match,
"regex_replace": BaseEvaluator.expr_regex_replace,
"list_flatten": BaseEvaluator.expr_flatten,
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
"ak_user_by": BaseEvaluator.expr_user_by,
"ak_logger": get_logger(),
@ -40,6 +41,15 @@ class BaseEvaluator:
self._context = {}
self._filename = "BaseEvalautor"
@staticmethod
def expr_flatten(value: list[Any] | Any) -> Optional[Any]:
"""Flatten `value` if its a list"""
if isinstance(value, list):
if len(value) < 1:
return None
return value[0]
return value
@staticmethod
def expr_regex_match(value: Any, regex: str) -> bool:
"""Expression Filter to run re.search"""

View File

@ -111,6 +111,7 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
"django_redis.cache",
"celery.backends.redis",
"celery.worker",
"paramiko.transport",
]:
return None
LOGGER.debug("sending event to sentry", exc=exc_value, source_logger=event.get("logger", None))

View File

@ -1,5 +1,4 @@
"""http helpers"""
from os import environ
from typing import Any, Optional
from django.http import HttpRequest
@ -7,7 +6,7 @@ from requests.sessions import Session
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik import ENV_GIT_HASH_KEY, __version__
from authentik import get_full_version
OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP"
OUTPOST_TOKEN_HEADER = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN" # nosec
@ -75,8 +74,7 @@ def get_client_ip(request: Optional[HttpRequest]) -> str:
def authentik_user_agent() -> str:
"""Get a common user agent"""
build = environ.get(ENV_GIT_HASH_KEY, "tagged")
return f"authentik@{__version__} (build={build})"
return f"authentik@{get_full_version()}"
def get_http_session() -> Session:

View File

@ -58,4 +58,6 @@ def get_env() -> str:
return "compose"
if CONFIG.y_bool("debug"):
return "dev"
if "AK_APPLIANCE" in os.environ:
return os.environ["AK_APPLIANCE"]
return "custom"

View File

@ -1,6 +1,4 @@
"""Outpost API Views"""
from os import environ
from dacite.core import from_dict
from dacite.exceptions import DaciteError
from django_filters.filters import ModelMultipleChoiceFilter
@ -14,7 +12,7 @@ from rest_framework.response import Response
from rest_framework.serializers import JSONField, ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik import ENV_GIT_HASH_KEY
from authentik import get_build_hash
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer, is_dict
@ -154,7 +152,7 @@ class OutpostViewSet(UsedByMixin, ModelViewSet):
"version_should": state.version_should,
"version_outdated": state.version_outdated,
"build_hash": state.build_hash,
"build_hash_should": environ.get(ENV_GIT_HASH_KEY, ""),
"build_hash_should": get_build_hash(),
}
)
return Response(OutpostHealthSerializer(states, many=True).data)

View File

@ -1,12 +1,11 @@
"""Base Controller"""
from dataclasses import dataclass
from os import environ
from typing import Optional
from structlog.stdlib import get_logger
from structlog.testing import capture_logs
from authentik import ENV_GIT_HASH_KEY, __version__
from authentik import __version__, get_build_hash
from authentik.lib.config import CONFIG
from authentik.lib.sentry import SentryIgnoredException
from authentik.outposts.models import (
@ -102,5 +101,5 @@ class BaseController:
return image_name_template % {
"type": self.outpost.type,
"version": __version__,
"build_hash": environ.get(ENV_GIT_HASH_KEY, ""),
"build_hash": get_build_hash(),
}

View File

@ -9,6 +9,7 @@ from docker import DockerClient as UpstreamDockerClient
from docker.errors import DockerException, NotFound
from docker.models.containers import Container
from docker.utils.utils import kwargs_from_env
from paramiko.ssh_exception import SSHException
from structlog.stdlib import get_logger
from yaml import safe_dump
@ -49,10 +50,13 @@ class DockerClient(UpstreamDockerClient, BaseClient):
authentication_kp=connection.tls_authentication,
)
tls_config = self.tls.write()
super().__init__(
base_url=connection.url,
tls=tls_config,
)
try:
super().__init__(
base_url=connection.url,
tls=tls_config,
)
except SSHException as exc:
raise ServiceConnectionInvalid from exc
self.logger = get_logger()
# Ensure the client actually works
self.containers.list()
@ -102,9 +106,12 @@ class DockerController(BaseController):
).lower()
def _get_labels(self) -> dict[str, str]:
return {
labels = {
"io.goauthentik.outpost-uuid": self.outpost.pk.hex,
}
if self.outpost.config.docker_labels:
labels.update(self.outpost.config.docker_labels)
return labels
def _get_env(self) -> dict[str, str]:
return {

View File

@ -18,6 +18,7 @@ from kubernetes.client import (
V1SecretKeySelector,
)
from authentik import __version__, get_full_version
from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
@ -52,15 +53,18 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
raise NeedsUpdate()
super().reconcile(current, reference)
def get_pod_meta(self) -> dict[str, str]:
def get_pod_meta(self, **kwargs) -> dict[str, str]:
"""Get common object metadata"""
return {
"app.kubernetes.io/name": "authentik-outpost",
"app.kubernetes.io/managed-by": "goauthentik.io",
"goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
"goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
"goauthentik.io/outpost-type": str(self.controller.outpost.type),
}
kwargs.update(
{
"app.kubernetes.io/name": f"authentik-outpost-{self.outpost.type}",
"app.kubernetes.io/managed-by": "goauthentik.io",
"goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
"goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
"goauthentik.io/outpost-type": str(self.controller.outpost.type),
}
)
return kwargs
def get_reference_object(self) -> V1Deployment:
"""Get deployment object for outpost"""
@ -77,13 +81,24 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
meta = self.get_object_meta(name=self.name)
image_name = self.controller.get_container_image()
image_pull_secrets = self.outpost.config.kubernetes_image_pull_secrets
version = get_full_version()
return V1Deployment(
metadata=meta,
spec=V1DeploymentSpec(
replicas=self.outpost.config.kubernetes_replicas,
selector=V1LabelSelector(match_labels=self.get_pod_meta()),
template=V1PodTemplateSpec(
metadata=V1ObjectMeta(labels=self.get_pod_meta()),
metadata=V1ObjectMeta(
labels=self.get_pod_meta(
**{
# Support istio-specific labels, but also use the standard k8s
# recommendations
"app.kubernetes.io/version": version,
"app": "authentik-outpost",
"version": version,
}
)
),
spec=V1PodSpec(
image_pull_secrets=[
V1ObjectReference(name=secret) for secret in image_pull_secrets

View File

@ -6,6 +6,7 @@ from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
from authentik.outposts.controllers.k8s.utils import compare_ports
if TYPE_CHECKING:
@ -25,6 +26,8 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
# after an authentik update. However the ports might have also changed during
# the update, so this causes the service to be re-created with higher
# priority than being updated.
if current.spec.selector != reference.spec.selector:
raise NeedsUpdate()
super().reconcile(current, reference)
def get_reference_object(self) -> V1Service:

View File

@ -1,7 +1,6 @@
"""Outpost models"""
from dataclasses import asdict, dataclass, field
from datetime import datetime
from os import environ
from typing import Iterable, Optional
from uuid import uuid4
@ -17,7 +16,7 @@ from model_utils.managers import InheritanceManager
from packaging.version import LegacyVersion, Version, parse
from structlog.stdlib import get_logger
from authentik import ENV_GIT_HASH_KEY, __version__
from authentik import __version__, get_build_hash
from authentik.core.models import (
USER_ATTRIBUTE_CAN_OVERRIDE_IP,
USER_ATTRIBUTE_SA,
@ -61,6 +60,7 @@ class OutpostConfig:
docker_network: Optional[str] = field(default=None)
docker_map_ports: bool = field(default=True)
docker_labels: Optional[dict[str, str]] = field(default=None)
container_image: Optional[str] = field(default=None)
@ -414,7 +414,7 @@ class OutpostState:
"""Check if outpost version matches our version"""
if not self.version:
return False
if self.build_hash != environ.get(ENV_GIT_HASH_KEY, ""):
if self.build_hash != get_build_hash():
return False
return parse(self.version) < OUR_VERSION

View File

@ -77,8 +77,12 @@ def outpost_service_connection_state(connection_pk: Any):
cls = DockerClient
if isinstance(connection, KubernetesServiceConnection):
cls = KubernetesClient
with cls(connection) as client:
state = client.fetch_state()
try:
with cls(connection) as client:
state = client.fetch_state()
except ServiceConnectionInvalid as exc:
LOGGER.warning("Failed to get client status", exc=exc)
return
cache.set(connection.state_key, state, timeout=None)

View File

@ -5,7 +5,7 @@ from typing import Iterator, Optional
from django.core.cache import cache
from django.http import HttpRequest
from prometheus_client import Histogram
from prometheus_client import Gauge, Histogram
from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span
from structlog.stdlib import BoundLogger, get_logger
@ -14,13 +14,11 @@ from authentik.core.models import User
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel, PolicyEngineMode
from authentik.policies.process import PolicyProcess, cache_key
from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.root.monitoring import UpdatingGauge
CURRENT_PROCESS = current_process()
GAUGE_POLICIES_CACHED = UpdatingGauge(
GAUGE_POLICIES_CACHED = Gauge(
"authentik_policies_cached",
"Cached Policies",
update_func=lambda: len(cache.keys("policy_*") or []),
)
HIST_POLICIES_BUILD_TIME = Histogram(
"authentik_policies_build_time",

View File

@ -45,7 +45,7 @@ class HaveIBeenPwendPolicy(Policy):
fields=request.context.keys(),
)
return PolicyResult(False, _("Password not set in context"))
password = request.context[self.password_field]
password = str(request.context[self.password_field])
pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec
url = f"https://api.pwnedpasswords.com/range/{pw_hash[:5]}"

View File

@ -5,10 +5,19 @@ from django.dispatch import receiver
from structlog.stdlib import get_logger
from authentik.core.api.applications import user_app_cache_key
from authentik.policies.engine import GAUGE_POLICIES_CACHED
from authentik.root.monitoring import monitoring_set
LOGGER = get_logger()
@receiver(monitoring_set)
# pylint: disable=unused-argument
def monitoring_set_policies(sender, **kwargs):
"""set policy gauges"""
GAUGE_POLICIES_CACHED.set(len(cache.keys("policy_*") or []))
@receiver(post_save)
# pylint: disable=unused-argument
def invalidate_policy_cache(sender, instance, **_):

View File

@ -99,7 +99,7 @@ class OAuthAuthorizationParams:
# and POST request.
query_dict = request.POST if request.method == "POST" else request.GET
state = query_dict.get("state")
redirect_uri = query_dict.get("redirect_uri", "")
redirect_uri = query_dict.get("redirect_uri", "").lower()
response_type = query_dict.get("response_type", "")
grant_type = None
@ -156,13 +156,20 @@ class OAuthAuthorizationParams:
if not self.redirect_uri:
LOGGER.warning("Missing redirect uri.")
raise RedirectUriError("", allowed_redirect_urls)
if len(allowed_redirect_urls) < 1:
if self.provider.redirect_uris == "":
LOGGER.info("Setting redirect for blank redirect_uris", redirect=self.redirect_uri)
self.provider.redirect_uris = self.redirect_uri
self.provider.save()
allowed_redirect_urls = self.provider.redirect_uris.split()
if self.provider.redirect_uris == "*":
LOGGER.warning(
"Provider has no allowed redirect_uri set, allowing all.",
allow=self.redirect_uri.lower(),
"Provider has wildcard allowed redirect_uri set, allowing all.",
allow=self.redirect_uri,
)
return
if self.redirect_uri.lower() not in [x.lower() for x in allowed_redirect_urls]:
if self.redirect_uri not in [x.lower() for x in allowed_redirect_urls]:
LOGGER.warning(
"Invalid redirect uri",
redirect_uri=self.redirect_uri,

View File

@ -66,7 +66,7 @@ class TokenParams:
provider=provider,
client_id=client_id,
client_secret=client_secret,
redirect_uri=request.POST.get("redirect_uri", ""),
redirect_uri=request.POST.get("redirect_uri", "").lower(),
grant_type=request.POST.get("grant_type", ""),
state=request.POST.get("state", ""),
scope=request.POST.get("scope", "").split(),
@ -123,21 +123,23 @@ class TokenParams:
LOGGER.warning("Invalid grant type", grant_type=self.grant_type)
raise TokenError("unsupported_grant_type")
def __post_init_code(self, raw_code):
def __post_init_code(self, raw_code: str):
if not raw_code:
LOGGER.warning("Missing authorization code")
raise TokenError("invalid_grant")
allowed_redirect_urls = self.provider.redirect_uris.split()
if len(allowed_redirect_urls) < 1:
if self.provider.redirect_uris == "*":
LOGGER.warning(
"Provider has no allowed redirect_uri set, allowing all.",
allow=self.redirect_uri.lower(),
"Provider has wildcard allowed redirect_uri set, allowing all.",
redirect=self.redirect_uri,
)
elif self.redirect_uri.lower() not in [x.lower() for x in allowed_redirect_urls]:
# At this point, no provider should have a blank redirect_uri, in case they do
# this will check an empty array and raise an error
elif self.redirect_uri not in [x.lower() for x in allowed_redirect_urls]:
LOGGER.warning(
"Invalid redirect uri",
uri=self.redirect_uri,
redirect=self.redirect_uri,
expected=self.provider.redirect_uris.split(),
)
raise TokenError("invalid_client")

View File

@ -23,10 +23,12 @@ class ProxyDockerController(DockerController):
proxy_provider: ProxyProvider
external_host_name = urlparse(proxy_provider.external_host)
hosts.append(f"`{external_host_name.netloc}`")
traefik_name = f"ak-outpost-{self.outpost.pk.hex}"
traefik_name = self.name
labels = super()._get_labels()
labels["traefik.enable"] = "true"
labels[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)}) && PathPrefix(`/akprox`)"
labels[f"traefik.http.routers.{traefik_name}-router.tls"] = "true"
labels[f"traefik.http.routers.{traefik_name}-router.service"] = f"{traefik_name}-service"
labels[

View File

@ -121,13 +121,6 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
forwardAuth=TraefikMiddlewareSpecForwardAuth(
address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik",
authResponseHeaders=[
# Legacy headers, remove after 2022.1
"X-Auth-Username",
"X-Auth-Groups",
"X-Forwarded-Email",
"X-Forwarded-Preferred-Username",
"X-Forwarded-User",
# New headers, unique prefix
"X-authentik-username",
"X-authentik-groups",
"X-authentik-email",

View File

@ -1,37 +1,17 @@
"""Metrics view"""
from base64 import b64encode
from typing import Callable
from django.conf import settings
from django.db import connections
from django.db.utils import OperationalError
from django.dispatch import Signal
from django.http import HttpRequest, HttpResponse
from django.views import View
from django_prometheus.exports import ExportToDjangoView
from django_redis import get_redis_connection
from prometheus_client import Gauge
from redis.exceptions import RedisError
from authentik.admin.api.workers import GAUGE_WORKERS
from authentik.events.monitored_tasks import TaskInfo
from authentik.root.celery import CELERY_APP
class UpdatingGauge(Gauge):
"""Gauge which fetches its own value from an update function.
Update function is called on instantiate"""
def __init__(self, *args, update_func: Callable, **kwargs):
super().__init__(*args, **kwargs)
self._update_func = update_func
self.update()
def update(self):
"""Set value from update function"""
val = self._update_func()
if val:
self.set(val)
monitoring_set = Signal()
class MetricsView(View):
@ -49,11 +29,7 @@ class MetricsView(View):
response["WWW-Authenticate"] = 'Basic realm="authentik-monitoring"'
return response
count = len(CELERY_APP.control.ping(timeout=0.5))
GAUGE_WORKERS.set(count)
for task in TaskInfo.all().values():
task.set_prom_metrics()
monitoring_set.send_robust(self)
return ExportToDjangoView(request)

View File

@ -1,14 +1,4 @@
"""
Django settings for authentik project.
Generated by 'django-admin startproject' using Django 2.1.3.
For more information on this file, see
https://docs.djangoproject.com/en/2.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.1/ref/settings/
"""
"""root settings for authentik"""
import importlib
import logging
@ -18,24 +8,22 @@ from hashlib import sha512
from json import dumps
from tempfile import gettempdir
from time import time
from urllib.parse import quote
from urllib.parse import quote_plus
import structlog
from celery.schedules import crontab
from sentry_sdk import init as sentry_init
from sentry_sdk.api import set_tag
from sentry_sdk.integrations.boto3 import Boto3Integration
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.threading import ThreadingIntegration
from authentik import ENV_GIT_HASH_KEY, __version__
from authentik import ENV_GIT_HASH_KEY, __version__, get_build_hash
from authentik.core.middleware import structlog_add_request_id
from authentik.lib.config import CONFIG
from authentik.lib.logging import add_process_id
from authentik.lib.sentry import before_send
from authentik.lib.utils.http import get_http_session
from authentik.lib.utils.reflection import get_env
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
@ -75,6 +63,7 @@ AUTH_USER_MODEL = "authentik_core.User"
_cookie_suffix = "_debug" if DEBUG else ""
CSRF_COOKIE_NAME = "authentik_csrf"
CSRF_HEADER_NAME = "HTTP_X_AUTHENTIK_CSRF"
LANGUAGE_COOKIE_NAME = f"authentik_language{_cookie_suffix}"
SESSION_COOKIE_NAME = f"authentik_session{_cookie_suffix}"
SESSION_COOKIE_DOMAIN = CONFIG.y("cookie_domain", None)
@ -164,9 +153,6 @@ SPECTACULAR_SETTINGS = {
{
"url": "/api/v3/",
},
{
"url": "/api/v2beta/",
},
],
"CONTACT": {
"email": "hello@beryju.org",
@ -222,7 +208,7 @@ if CONFIG.y_bool("redis.tls", False):
REDIS_CELERY_TLS_REQUIREMENTS = f"?ssl_cert_reqs={CONFIG.y('redis.tls_reqs')}"
_redis_url = (
f"{REDIS_PROTOCOL_PREFIX}:"
f"{quote(CONFIG.y('redis.password'))}@{quote(CONFIG.y('redis.host'))}:"
f"{quote_plus(CONFIG.y('redis.password'))}@{quote_plus(CONFIG.y('redis.host'))}:"
f"{int(CONFIG.y('redis.port'))}"
)
@ -349,6 +335,7 @@ LOCALE_PATHS = ["./locale"]
# Celery settings
# Add a 10 minute timeout to all Celery tasks.
CELERY_TASK_SOFT_TIME_LIMIT = 600
CELERY_WORKER_MAX_TASKS_PER_CHILD = 50
CELERY_BEAT_SCHEDULE = {
"clean_expired_models": {
"task": "authentik.core.tasks.clean_expired_models",
@ -357,7 +344,7 @@ CELERY_BEAT_SCHEDULE = {
},
"db_backup": {
"task": "authentik.core.tasks.backup_database",
"schedule": crontab(hour="*/24"),
"schedule": crontab(hour="*/24", minute=0),
"options": {"queue": "authentik_scheduled"},
},
}
@ -378,7 +365,7 @@ DBBACKUP_CONNECTOR_MAPPING = {
"django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
}
DBBACKUP_TMP_DIR = gettempdir() if DEBUG else "/tmp" # nosec
DBBACKUP_CLEANUP_KEEP = 30
DBBACKUP_CLEANUP_KEEP = 10
if CONFIG.y("postgresql.s3_backup.bucket", "") != "":
DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
DBBACKUP_STORAGE_OPTIONS = {
@ -398,10 +385,6 @@ if CONFIG.y("postgresql.s3_backup.bucket", "") != "":
# Sentry integration
SENTRY_DSN = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8"
# Default to empty string as that is what docker has
build_hash = os.environ.get(ENV_GIT_HASH_KEY, "")
if build_hash == "":
build_hash = "tagged"
env = get_env()
_ERROR_REPORTING = CONFIG.y_bool("error_reporting.enabled", False)
@ -413,7 +396,6 @@ if _ERROR_REPORTING:
DjangoIntegration(transaction_style="function_name"),
CeleryIntegration(),
RedisIntegration(),
Boto3Integration(),
ThreadingIntegration(propagate_hub=True),
],
before_send=before_send,
@ -422,35 +404,14 @@ if _ERROR_REPORTING:
environment=CONFIG.y("error_reporting.environment", "customer"),
send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False),
)
set_tag("authentik.build_hash", build_hash)
set_tag("authentik.build_hash", get_build_hash("tagged"))
set_tag("authentik.env", env)
set_tag("authentik.component", "backend")
set_tag("authentik.uuid", sha512(SECRET_KEY.encode("ascii")).hexdigest()[:16])
set_tag("authentik.uuid", sha512(str(SECRET_KEY).encode("ascii")).hexdigest()[:16])
j_print(
"Error reporting is enabled",
env=CONFIG.y("error_reporting.environment", "customer"),
)
if not CONFIG.y_bool("disable_startup_analytics", False):
should_send = env not in ["dev", "ci"]
if should_send:
try:
get_http_session().post(
"https://goauthentik.io/api/event",
json={
"domain": "authentik",
"name": "pageview",
"referrer": f"{__version__} ({build_hash})",
"url": f"http://localhost/{env}?utm_source={__version__}&utm_medium={env}",
},
headers={
"User-Agent": sha512(SECRET_KEY.encode("ascii")).hexdigest()[:16],
"Content-Type": "application/json",
},
timeout=5,
)
# pylint: disable=bare-except
except: # nosec
pass
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/

View File

@ -35,21 +35,21 @@ class LDAPProviderManager(ObjectManager):
"goauthentik.io/sources/ldap/ms-userprincipalname",
name="authentik default Active Directory Mapping: userPrincipalName",
object_field="attributes.upn",
expression="return ldap.get('userPrincipalName')",
expression="return list_flatten(ldap.get('userPrincipalName'))",
),
EnsureExists(
LDAPPropertyMapping,
"goauthentik.io/sources/ldap/ms-givenName",
name="authentik default Active Directory Mapping: givenName",
object_field="attributes.givenName",
expression="return ldap.get('givenName')",
expression="return list_flatten(ldap.get('givenName'))",
),
EnsureExists(
LDAPPropertyMapping,
"goauthentik.io/sources/ldap/ms-sn",
name="authentik default Active Directory Mapping: sn",
object_field="attributes.sn",
expression="return ldap.get('sn')",
expression="return list_flatten(ldap.get('sn'))",
),
# OpenLDAP specific mappings
EnsureExists(

View File

@ -18,7 +18,12 @@ class AuthenticateWebAuthnStageSerializer(StageSerializer):
class Meta:
model = AuthenticateWebAuthnStage
fields = StageSerializer.Meta.fields + ["configure_flow", "user_verification"]
fields = StageSerializer.Meta.fields + [
"configure_flow",
"user_verification",
"authenticator_attachment",
"resident_key_requirement",
]
class AuthenticateWebAuthnStageViewSet(UsedByMixin, ModelViewSet):

View File

@ -0,0 +1,37 @@
# Generated by Django 4.0.1 on 2022-01-12 21:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_stages_authenticator_webauthn",
"0005_authenticatewebauthnstage_user_verification",
),
]
operations = [
migrations.AddField(
model_name="authenticatewebauthnstage",
name="authenticator_attachment",
field=models.TextField(
choices=[("platform", "Platform"), ("cross-platform", "Cross Platform")],
default=None,
null=True,
),
),
migrations.AddField(
model_name="authenticatewebauthnstage",
name="resident_key_requirement",
field=models.TextField(
choices=[
("discouraged", "Discouraged"),
("preferred", "Preferred"),
("required", "Required"),
],
default="preferred",
),
),
]

View File

@ -31,6 +31,40 @@ class UserVerification(models.TextChoices):
DISCOURAGED = "discouraged"
class ResidentKeyRequirement(models.TextChoices):
"""The Relying Party's preference for the authenticator to create a dedicated "client-side"
credential for it. Requiring an authenticator to store a dedicated credential should not be
done lightly due to the limited storage capacity of some types of authenticators.
Members:
`DISCOURAGED`: The authenticator should not create a dedicated credential
`PREFERRED`: The authenticator can create and store a dedicated credential, but if it
doesn't that's alright too
`REQUIRED`: The authenticator MUST create a dedicated credential. If it cannot, the RP
is prepared for an error to occur.
https://www.w3.org/TR/webauthn-2/#enum-residentKeyRequirement
"""
DISCOURAGED = "discouraged"
PREFERRED = "preferred"
REQUIRED = "required"
class AuthenticatorAttachment(models.TextChoices):
"""How an authenticator is connected to the client/browser.
Members:
`PLATFORM`: A non-removable authenticator, like TouchID or Windows Hello
`CROSS_PLATFORM`: A "roaming" authenticator, like a YubiKey
https://www.w3.org/TR/webauthn-2/#enumdef-authenticatorattachment
"""
PLATFORM = "platform"
CROSS_PLATFORM = "cross-platform"
class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
"""WebAuthn stage"""
@ -38,6 +72,13 @@ class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
choices=UserVerification.choices,
default=UserVerification.PREFERRED,
)
resident_key_requirement = models.TextField(
choices=ResidentKeyRequirement.choices,
default=ResidentKeyRequirement.PREFERRED,
)
authenticator_attachment = models.TextField(
choices=AuthenticatorAttachment.choices, default=None, null=True
)
@property
def serializer(self) -> BaseSerializer:

View File

@ -13,7 +13,6 @@ from webauthn.helpers.structs import (
AuthenticatorSelectionCriteria,
PublicKeyCredentialCreationOptions,
RegistrationCredential,
ResidentKeyRequirement,
)
from webauthn.registration.verify_registration_response import VerifiedRegistration
@ -85,6 +84,12 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
stage: AuthenticateWebAuthnStage = self.executor.current_stage
user = self.get_pending_user()
# library accepts none so we store null in the database, but if there is a value
# set, cast it to string to ensure it's not a django class
authenticator_attachment = stage.authenticator_attachment
if authenticator_attachment:
authenticator_attachment = str(authenticator_attachment)
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
rp_id=get_rp_id(self.request),
rp_name=self.request.tenant.branding_title,
@ -92,8 +97,9 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
user_name=user.username,
user_display_name=user.name,
authenticator_selection=AuthenticatorSelectionCriteria(
resident_key=ResidentKeyRequirement.PREFERRED,
resident_key=str(stage.resident_key_requirement),
user_verification=str(stage.user_verification),
authenticator_attachment=authenticator_attachment,
),
)

View File

@ -8,6 +8,7 @@ import (
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/common"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ldap"
)
@ -27,6 +28,7 @@ func main() {
log.FieldKeyTime: "timestamp",
},
})
go debug.EnableDebugServer()
akURL, found := os.LookupEnv("AUTHENTIK_HOST")
if !found {
fmt.Println("env AUTHENTIK_HOST not set!")

View File

@ -9,6 +9,7 @@ import (
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/common"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/proxyv2"
)
@ -32,6 +33,7 @@ func main() {
log.FieldKeyTime: "timestamp",
},
})
go debug.EnableDebugServer()
akURL, found := os.LookupEnv("AUTHENTIK_HOST")
if !found {
fmt.Println("env AUTHENTIK_HOST not set!")

View File

@ -11,6 +11,7 @@ import (
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/gounicorn"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/proxyv2"
@ -28,6 +29,7 @@ func main() {
log.FieldKeyTime: "timestamp",
},
})
go debug.EnableDebugServer()
l := log.WithField("logger", "authentik.root")
config.DefaultConfig()
err := config.LoadConfig("./authentik/lib/default.yml")

View File

@ -17,7 +17,7 @@ services:
image: redis:alpine
restart: unless-stopped
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.12.5}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.4}
restart: unless-stopped
command: server
environment:
@ -35,10 +35,10 @@ services:
env_file:
- .env
ports:
- "0.0.0.0:9000:9000"
- "0.0.0.0:9443:9443"
- "0.0.0.0:${AUTHENTIK_PORT_HTTP:-9000}:9000"
- "0.0.0.0:${AUTHENTIK_PORT_HTTPS:-9443}:9443"
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.12.5}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.4}
restart: unless-stopped
command: worker
environment:

11
go.mod
View File

@ -9,10 +9,9 @@ require (
github.com/garyburd/redigo v1.6.2 // indirect
github.com/getsentry/sentry-go v0.12.0
github.com/go-ldap/ldap/v3 v3.4.1
github.com/go-openapi/runtime v0.21.0
github.com/go-openapi/runtime v0.22.0
github.com/go-openapi/strfmt v0.21.1
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
@ -26,10 +25,12 @@ require (
github.com/pires/go-proxyproto v0.6.1
github.com/pkg/errors v0.9.1
github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/client_golang v1.12.1
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b
github.com/sirupsen/logrus v1.8.1
goauthentik.io/api v0.2021124.9
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558
github.com/stretchr/testify v1.7.0
goauthentik.io/api v0.2021125.1
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b

32
go.sum
View File

@ -70,8 +70,9 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -182,8 +183,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.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.21.0 h1:giZ8eT26R+/rx6RX2MkYjZPY8vPYVKDhP/mOazrQHzM=
github.com/go-openapi/runtime v0.21.0/go.mod h1:aQg+kaIQEn+A2CRSY1TxbM8+sT9g2V3aLc1FbIAnbbs=
github.com/go-openapi/runtime v0.22.0 h1:vY2D0u807kkcwidaj0YJuq4zyAWQnjLNDpJcVBrUFNs=
github.com/go-openapi/runtime v0.22.0/go.mod h1:aQg+kaIQEn+A2CRSY1TxbM8+sT9g2V3aLc1FbIAnbbs=
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.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
@ -360,6 +361,7 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
@ -427,6 +429,7 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@ -467,8 +470,9 @@ github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac/go.mod h1:hoL
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -476,13 +480,17 @@ github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@ -562,8 +570,8 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
goauthentik.io/api v0.2021124.9 h1:l6kzTggi8lt+Vmm5js7oxp9U/bXoOEIl4565bUeepuM=
goauthentik.io/api v0.2021124.9/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
goauthentik.io/api v0.2021125.1 h1:Ja00N1D1wjqFiR90JV8nDayhmm39uLudzsJhwCjoQJ4=
goauthentik.io/api v0.2021125.1/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -655,6 +663,7 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211008194852-3b03d305991f h1:1scJEYZBaF48BaG6tYbtxmLcXqwYGSfGcMoStTqkkIw=
golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -663,8 +672,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 h1:D7nTwh4J0i+5mW4Zjzn5omvlr6YBcWywE6KOcatyNxY=
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -731,8 +740,9 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -5,16 +5,24 @@ import (
"os"
)
func BUILD() string {
func BUILD(def string) string {
build := os.Getenv("GIT_BUILD_HASH")
if build == "" {
return "tagged"
return def
}
return build
}
func OutpostUserAgent() string {
return fmt.Sprintf("authentik-outpost@%s (build=%s)", VERSION, BUILD())
func FullVersion() string {
ver := VERSION
if b := BUILD(""); b != "" {
ver = fmt.Sprintf("%s.%s", ver, b)
}
return ver
}
const VERSION = "2021.12.5"
func OutpostUserAgent() string {
return fmt.Sprintf("authentik-outpost@%s", FullVersion())
}
const VERSION = "2022.1.4"

24
internal/debug/debug.go Normal file
View File

@ -0,0 +1,24 @@
package debug
import (
"net/http"
"net/http/pprof"
"os"
"strings"
log "github.com/sirupsen/logrus"
)
func EnableDebugServer() {
l := log.WithField("logger", "authentik.go_debugger")
if deb := os.Getenv("AUTHENTIK_DEBUG"); strings.ToLower(deb) != "true" {
l.Info("not enabling debug server, set `AUTHENTIK_DEBUG` to `true` to enable it.")
}
h := http.NewServeMux()
h.HandleFunc("/debug/pprof/", pprof.Index)
h.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
h.HandleFunc("/debug/pprof/profile", pprof.Profile)
h.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
h.HandleFunc("/debug/pprof/trace", pprof.Trace)
l.Println(http.ListenAndServe("0.0.0.0:9900", nil))
}

View File

@ -171,7 +171,7 @@ func (a *APIController) StartBackgorundTasks() error {
"outpost_type": a.Server.Type(),
"uuid": a.instanceUUID.String(),
"version": constants.VERSION,
"build": constants.BUILD(),
"build": constants.BUILD("tagged"),
}).Set(1)
go func() {
a.logger.Debug("Starting WS Handler...")

View File

@ -51,7 +51,7 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
Instruction: WebsocketInstructionHello,
Args: map[string]interface{}{
"version": constants.VERSION,
"buildHash": constants.BUILD(),
"buildHash": constants.BUILD("tagged"),
"uuid": ac.instanceUUID.String(),
},
}
@ -151,7 +151,7 @@ func (ac *APIController) startWSHandler() {
"outpost_type": ac.Server.Type(),
"uuid": ac.instanceUUID.String(),
"version": constants.VERSION,
"build": constants.BUILD(),
"build": constants.BUILD("tagged"),
}).SetToCurrentTime()
}
}
@ -165,7 +165,7 @@ func (ac *APIController) startWSHealth() {
Instruction: WebsocketInstructionHello,
Args: map[string]interface{}{
"version": constants.VERSION,
"buildHash": constants.BUILD(),
"buildHash": constants.BUILD("tagged"),
"uuid": ac.instanceUUID.String(),
},
}
@ -205,7 +205,7 @@ func (ac *APIController) startIntervalUpdater() {
"outpost_type": ac.Server.Type(),
"uuid": ac.instanceUUID.String(),
"version": constants.VERSION,
"build": constants.BUILD(),
"build": constants.BUILD("tagged"),
}).SetToCurrentTime()
}
}

View File

@ -34,7 +34,7 @@ func doGlobalSetup(outpost api.Outpost, globalConfig api.Config) {
} else {
l.Debug("Managed outpost, not setting global log level")
}
l.WithField("hash", constants.BUILD()).WithField("version", constants.VERSION).Info("Starting authentik outpost")
l.WithField("hash", constants.BUILD("tagged")).WithField("version", constants.VERSION).Info("Starting authentik outpost")
if globalConfig.ErrorReporting.Enabled {
dsn := "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8"

View File

@ -0,0 +1,66 @@
package ak
import (
"encoding/base64"
"fmt"
"math/rand"
"net/http"
"time"
"github.com/google/uuid"
"github.com/gorilla/securecookie"
log "github.com/sirupsen/logrus"
"goauthentik.io/api"
)
func TestSecret() string {
return base64.RawURLEncoding.EncodeToString(securecookie.GenerateRandomKey(32))
}
func MockConfig() api.Config {
return *api.NewConfig(
*api.NewErrorReportingConfig(false, "test", false, 0.0),
[]api.CapabilitiesEnum{},
100,
100,
100,
100,
)
}
func MockAK(outpost api.Outpost, globalConfig api.Config) *APIController {
config := api.NewConfiguration()
config.HTTPClient = &http.Client{
Transport: GetTLSTransport(),
}
token := TestSecret()
config.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token))
// create the API client, with the transport
apiClient := api.NewAPIClient(config)
log := log.WithField("logger", "authentik.outpost.ak-api-controller")
log.WithField("name", outpost.Name).Debug("Fetched outpost configuration")
log.Debug("Fetched global configuration")
// doGlobalSetup is called by the OnRefresh handler, which ticks on start
// doGlobalSetup(outpost, akConfig)
ac := &APIController{
Client: apiClient,
GlobalConfig: globalConfig,
token: token,
logger: log,
reloadOffset: time.Duration(rand.Intn(10)) * time.Second,
instanceUUID: uuid.New(),
Outpost: outpost,
wsBackoffMultiplier: 1,
refreshHandlers: make([]func(), 0),
}
ac.logger.WithField("offset", ac.reloadOffset.String()).Debug("HA Reload offset")
return ac
}

View File

@ -16,6 +16,7 @@ import (
"goauthentik.io/internal/outpost/ldap/flags"
"goauthentik.io/internal/outpost/ldap/metrics"
"goauthentik.io/internal/outpost/ldap/server"
"goauthentik.io/internal/outpost/ldap/utils"
)
const ContextUserKey = "ak_user"
@ -35,7 +36,7 @@ func NewDirectBinder(si server.LDAPServerInstance) *DirectBinder {
}
func (db *DirectBinder) GetUsername(dn string) (string, error) {
if !strings.HasSuffix(strings.ToLower(dn), strings.ToLower(db.si.GetBaseDN())) {
if !utils.HasSuffixNoCase(dn, db.si.GetBaseDN()) {
return "", errors.New("invalid base DN")
}
dns, err := goldap.ParseDN(dn)

View File

@ -12,11 +12,7 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
attrs := utils.AKAttrsToLDAP(u.Attributes)
attrs = utils.EnsureAttributes(attrs, map[string][]string{
"memberOf": pi.GroupsForUser(u),
// Old fields for backwards compatibility
"accountStatus": {utils.BoolToString(*u.IsActive)},
"superuser": {utils.BoolToString(u.IsSuperuser)},
// End old fields
"memberOf": pi.GroupsForUser(u),
"goauthentik.io/ldap/active": {utils.BoolToString(*u.IsActive)},
"goauthentik.io/ldap/superuser": {utils.BoolToString(u.IsSuperuser)},
"cn": {u.Username},

View File

@ -124,7 +124,7 @@ func (pi *ProviderInstance) GetBaseEntry() *ldap.Entry {
},
{
Name: "vendorVersion",
Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s (build %s)", constants.VERSION, constants.BUILD())},
Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s", constants.FullVersion())},
},
},
}
@ -140,26 +140,26 @@ func (pi *ProviderInstance) GetNeededObjects(scope int, baseDN string, filterOC
// If our requested base DN doesn't match any of the container DNs, then
// we're probably loading a user or group. If it does, then make sure our
// scope will eventually take us to users or groups.
if (baseDN == pi.BaseDN || strings.HasSuffix(baseDN, pi.UserDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetUserOCs()) {
if (strings.EqualFold(baseDN, pi.BaseDN) || utils.HasSuffixNoCase(baseDN, pi.UserDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetUserOCs()) {
if baseDN != pi.UserDN && baseDN != pi.BaseDN ||
baseDN == pi.BaseDN && scope > 1 ||
baseDN == pi.UserDN && scope > 0 {
strings.EqualFold(baseDN, pi.BaseDN) && scope > 1 ||
strings.EqualFold(baseDN, pi.UserDN) && scope > 0 {
needUsers = true
}
}
if (baseDN == pi.BaseDN || strings.HasSuffix(baseDN, pi.GroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetGroupOCs()) {
if (strings.EqualFold(baseDN, pi.BaseDN) || utils.HasSuffixNoCase(baseDN, pi.GroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetGroupOCs()) {
if baseDN != pi.GroupDN && baseDN != pi.BaseDN ||
baseDN == pi.BaseDN && scope > 1 ||
baseDN == pi.GroupDN && scope > 0 {
strings.EqualFold(baseDN, pi.BaseDN) && scope > 1 ||
strings.EqualFold(baseDN, pi.GroupDN) && scope > 0 {
needGroups = true
}
}
if (baseDN == pi.BaseDN || strings.HasSuffix(baseDN, pi.VirtualGroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetVirtualGroupOCs()) {
if (strings.EqualFold(baseDN, pi.BaseDN) || utils.HasSuffixNoCase(baseDN, pi.VirtualGroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetVirtualGroupOCs()) {
if baseDN != pi.VirtualGroupDN && baseDN != pi.BaseDN ||
baseDN == pi.BaseDN && scope > 1 ||
baseDN == pi.VirtualGroupDN && scope > 0 {
strings.EqualFold(baseDN, pi.BaseDN) && scope > 1 ||
strings.EqualFold(baseDN, pi.VirtualGroupDN) && scope > 0 {
needUsers = true
}
}

View File

@ -44,7 +44,7 @@ func (ds *DirectSearcher) SearchBase(req *search.Request, authz bool) (ldap.Serv
},
{
Name: "vendorVersion",
Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s (build %s)", constants.VERSION, constants.BUILD())},
Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s", constants.FullVersion())},
},
},
},

View File

@ -36,7 +36,7 @@ func NewDirectSearcher(si server.LDAPServerInstance) *DirectSearcher {
func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) {
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
baseDN := strings.ToLower(ds.si.GetBaseDN())
baseDN := ds.si.GetBaseDN()
filterOC, err := ldap.GetFilterObjectClass(req.Filter)
if err != nil {
@ -59,7 +59,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
}
if !strings.HasSuffix(req.BindDN, ","+baseDN) {
if !utils.HasSuffixNoCase(req.BindDN, ","+baseDN) {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": ds.si.GetOutpostName(),
"type": "search",
@ -105,7 +105,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
scope := req.SearchRequest.Scope
needUsers, needGroups := ds.si.GetNeededObjects(scope, req.BaseDN, filterOC)
if scope >= 0 && req.BaseDN == baseDN {
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) {
entries = append(entries, ds.si.GetBaseEntry())
}
@ -209,8 +209,8 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, err
}
if scope >= 0 && (req.BaseDN == ds.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ds.si.GetBaseUserDN())) {
singleu := strings.HasSuffix(req.BaseDN, ","+ds.si.GetBaseUserDN())
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseUserDN())) {
singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseUserDN())
if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseUserDN(), constants.OUUsers))
@ -220,7 +220,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) {
for _, u := range *users {
entry := ds.si.UserEntry(u)
if req.BaseDN == entry.DN || !singleu {
if strings.EqualFold(req.BaseDN, entry.DN) || !singleu {
entries = append(entries, entry)
}
}
@ -229,8 +229,8 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
scope += 1 // Return the scope to what it was before we descended
}
if scope >= 0 && (req.BaseDN == ds.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ds.si.GetBaseGroupDN())) {
singleg := strings.HasSuffix(req.BaseDN, ","+ds.si.GetBaseGroupDN())
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseGroupDN())) {
singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseGroupDN())
if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseGroupDN(), constants.OUGroups))
@ -240,7 +240,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) {
for _, g := range *groups {
entry := group.FromAPIGroup(g, ds.si).Entry()
if req.BaseDN == entry.DN || !singleg {
if strings.EqualFold(req.BaseDN, entry.DN) || !singleg {
entries = append(entries, entry)
}
}
@ -249,8 +249,8 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
scope += 1 // Return the scope to what it was before we descended
}
if scope >= 0 && (req.BaseDN == ds.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ds.si.GetBaseVirtualGroupDN())) {
singlevg := strings.HasSuffix(req.BaseDN, ","+ds.si.GetBaseVirtualGroupDN())
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseVirtualGroupDN())) {
singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseVirtualGroupDN())
if !singlevg || utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
@ -260,7 +260,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) {
for _, u := range *users {
entry := group.FromAPIUser(u, ds.si).Entry()
if req.BaseDN == entry.DN || !singlevg {
if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg {
entries = append(entries, entry)
}
}

View File

@ -39,7 +39,7 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) {
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
baseDN := strings.ToLower(ms.si.GetBaseDN())
baseDN := ms.si.GetBaseDN()
filterOC, err := ldap.GetFilterObjectClass(req.Filter)
if err != nil {
@ -62,7 +62,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
}
if !strings.HasSuffix(req.BindDN, ","+baseDN) {
if !utils.HasSuffixNoCase(req.BindDN, ","+baseDN) {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": ms.si.GetOutpostName(),
"type": "search",
@ -92,7 +92,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
scope := req.SearchRequest.Scope
needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, filterOC)
if scope >= 0 && req.BaseDN == baseDN {
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) {
entries = append(entries, ms.si.GetBaseEntry())
}
@ -139,7 +139,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
// as a member.
for _, u := range g.UsersObj {
if flags.UserPk == u.Pk {
// TODO: Is there a better way to clone this object?
//TODO: Is there a better way to clone this object?
fg := api.NewGroup(g.Pk, g.Name, g.Parent, g.ParentName, []int32{flags.UserPk}, []api.GroupMember{u})
fg.SetAttributes(*g.Attributes)
fg.SetIsSuperuser(*g.IsSuperuser)
@ -155,8 +155,8 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, err
}
if scope >= 0 && (req.BaseDN == ms.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ms.si.GetBaseUserDN())) {
singleu := strings.HasSuffix(req.BaseDN, ","+ms.si.GetBaseUserDN())
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseUserDN())) {
singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseUserDN())
if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseUserDN(), constants.OUUsers))
@ -166,7 +166,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) {
for _, u := range *users {
entry := ms.si.UserEntry(u)
if req.BaseDN == entry.DN || !singleu {
if strings.EqualFold(req.BaseDN, entry.DN) || !singleu {
entries = append(entries, entry)
}
}
@ -175,8 +175,8 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
scope += 1 // Return the scope to what it was before we descended
}
if scope >= 0 && (req.BaseDN == ms.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ms.si.GetBaseGroupDN())) {
singleg := strings.HasSuffix(req.BaseDN, ","+ms.si.GetBaseGroupDN())
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseGroupDN())) {
singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseGroupDN())
if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseGroupDN(), constants.OUGroups))
@ -185,7 +185,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) {
for _, g := range groups {
if req.BaseDN == g.DN || !singleg {
if strings.EqualFold(req.BaseDN, g.DN) || !singleg {
entries = append(entries, g.Entry())
}
}
@ -194,8 +194,8 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
scope += 1 // Return the scope to what it was before we descended
}
if scope >= 0 && (req.BaseDN == ms.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ms.si.GetBaseVirtualGroupDN())) {
singlevg := strings.HasSuffix(req.BaseDN, ","+ms.si.GetBaseVirtualGroupDN())
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseVirtualGroupDN())) {
singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseVirtualGroupDN())
if !singlevg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
@ -205,7 +205,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) {
for _, u := range *users {
entry := group.FromAPIUser(u, ms.si).Entry()
if req.BaseDN == entry.DN || !singlevg {
if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg {
entries = append(entries, entry)
}
}

View File

@ -26,7 +26,6 @@ type Request struct {
func NewRequest(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (*Request, *sentry.Span) {
rid := uuid.New().String()
bindDN = strings.ToLower(bindDN)
searchReq.BaseDN = strings.ToLower(searchReq.BaseDN)
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.search", sentry.TransactionName("authentik.providers.ldap.search"))
span.Description = fmt.Sprintf("%s (%s)", searchReq.BaseDN, ldap.ScopeMap[searchReq.Scope])
span.SetTag("request_uid", rid)

View File

@ -2,6 +2,7 @@ package utils
import (
"reflect"
"strings"
"github.com/nmcclain/ldap"
log "github.com/sirupsen/logrus"
@ -117,3 +118,7 @@ func GetContainerEntry(filterOC string, dn string, ou string) *ldap.Entry {
return nil
}
func HasSuffixNoCase(s1 string, s2 string) bool {
return strings.HasSuffix(strings.ToLower(s1), strings.ToLower(s2))
}

View File

@ -1,6 +1,8 @@
package utils
import (
"strings"
goldap "github.com/go-ldap/ldap/v3"
ber "github.com/nmcclain/asn1-ber"
"github.com/nmcclain/ldap"
@ -41,7 +43,7 @@ func parseFilterForGroupSingle(req api.ApiCoreGroupsListRequest, f *ber.Packet)
// Switch on type of the value, then check the key
switch vv := v.(type) {
case string:
switch k {
switch strings.ToLower(k.(string)) {
case "cn":
return req.Name(vv), false
case "member":
@ -54,14 +56,14 @@ func parseFilterForGroupSingle(req api.ApiCoreGroupsListRequest, f *ber.Packet)
username := userDN.RDNs[0].Attributes[0].Value
// If the DN's first ou is virtual-groups, ignore this filter
if len(userDN.RDNs) > 1 {
if userDN.RDNs[1].Attributes[0].Value == constants.OUVirtualGroups || userDN.RDNs[1].Attributes[0].Value == constants.OUGroups {
if strings.EqualFold(userDN.RDNs[1].Attributes[0].Value, constants.OUVirtualGroups) || strings.EqualFold(userDN.RDNs[1].Attributes[0].Value, constants.OUGroups) {
// Since we know we're not filtering anything, skip this request
return req, true
}
}
return req.MembersByUsername([]string{username}), false
}
// TODO: Support int
//TODO: Support int
default:
return req, false
}

View File

@ -66,7 +66,7 @@ func parseFilterForUserSingle(req api.ApiCoreUsersListRequest, f *ber.Packet) (a
}
return req.GroupsByName([]string{name}), false
}
// TODO: Support int
//TODO: Support int
default:
return req, false
}

View File

@ -46,6 +46,7 @@ type Application struct {
log *log.Entry
mux *mux.Router
ak *ak.APIController
errorTemplates *template.Template
}
@ -84,7 +85,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
mux := mux.NewRouter()
a := &Application{
Host: externalHost.Host,
log: log.WithField("logger", "authentik.outpost.proxy.bundle").WithField("provider", p.Name),
log: muxLogger,
outpostName: ak.Outpost.Name,
endpint: endpoint,
oauthConfig: oauth2Config,
@ -93,6 +94,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
httpClient: c,
mux: mux,
errorTemplates: templates.GetTemplates(),
ak: ak,
}
a.sessions = a.getStore(p)
mux.Use(web.NewLoggingHandler(muxLogger, func(l *log.Entry, r *http.Request) *log.Entry {
@ -172,7 +174,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
for _, regex := range strings.Split(*p.SkipPathRegex, "\n") {
re, err := regexp.Compile(regex)
if err != nil {
// TODO: maybe create event for this?
//TODO: maybe create event for this?
a.log.WithError(err).Warning("failed to compile SkipPathRegex")
continue
} else {
@ -187,12 +189,16 @@ func (a *Application) Mode() api.ProxyMode {
return *a.proxyConfig.Mode
}
func (a *Application) ProxyConfig() api.ProxyOutpostConfig {
return a.proxyConfig
}
func (a *Application) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
a.mux.ServeHTTP(rw, r)
}
func (a *Application) handleSignOut(rw http.ResponseWriter, r *http.Request) {
// TODO: Token revocation
//TODO: Token revocation
s, err := a.sessions.Get(r, constants.SeesionName)
if err != nil {
http.Redirect(rw, r, a.endpint.EndSessionEndpoint, http.StatusFound)

View File

@ -1,7 +1,8 @@
package application
type ProxyClaims struct {
UserAttributes map[string]interface{} `json:"user_attributes"`
UserAttributes map[string]interface{} `json:"user_attributes"`
BackendOverride string `json:"backend_override"`
}
type Claims struct {

View File

@ -13,15 +13,18 @@ import (
type OIDCEndpoint struct {
oauth2.Endpoint
EndSessionEndpoint string
JwksUri string
}
func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string) OIDCEndpoint {
authUrl := p.OidcConfiguration.AuthorizationEndpoint
endUrl := p.OidcConfiguration.EndSessionEndpoint
jwksUrl := p.OidcConfiguration.JwksUri
if browserHost, found := os.LookupEnv("AUTHENTIK_HOST_BROWSER"); found && browserHost != "" {
host := os.Getenv("AUTHENTIK_HOST")
authUrl = strings.ReplaceAll(authUrl, host, browserHost)
endUrl = strings.ReplaceAll(endUrl, host, browserHost)
jwksUrl = strings.ReplaceAll(jwksUrl, host, browserHost)
}
ep := OIDCEndpoint{
Endpoint: oauth2.Endpoint{
@ -30,6 +33,7 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string) OIDCEndpoin
AuthStyle: oauth2.AuthStyleInParams,
},
EndSessionEndpoint: endUrl,
JwksUri: jwksUrl,
}
authU, err := url.Parse(authUrl)
if err != nil {
@ -39,6 +43,10 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string) OIDCEndpoin
if err != nil {
return ep
}
jwksU, err := url.Parse(jwksUrl)
if err != nil {
return ep
}
if authU.Host != "localhost:8000" {
return ep
}
@ -54,7 +62,10 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string) OIDCEndpoin
authU.Scheme = aku.Scheme
endU.Host = aku.Host
endU.Scheme = aku.Scheme
jwksU.Host = aku.Host
jwksU.Scheme = aku.Scheme
ep.AuthURL = authU.String()
ep.EndSessionEndpoint = endU.String()
ep.JwksUri = jwksU.String()
return ep
}

View File

@ -1,7 +1,9 @@
package application
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/url"
@ -14,14 +16,6 @@ import (
func (a *Application) addHeaders(headers http.Header, c *Claims) {
// https://goauthentik.io/docs/providers/proxy/proxy
// Legacy headers, remove after 2022.1
headers.Set("X-Auth-Username", c.PreferredUsername)
headers.Set("X-Auth-Groups", strings.Join(c.Groups, "|"))
headers.Set("X-Forwarded-Email", c.Email)
headers.Set("X-Forwarded-Preferred-Username", c.PreferredUsername)
headers.Set("X-Forwarded-User", c.Sub)
// New headers, unique prefix
headers.Set("X-authentik-username", c.PreferredUsername)
headers.Set("X-authentik-groups", strings.Join(c.Groups, "|"))
headers.Set("X-authentik-email", c.Email)
@ -30,7 +24,7 @@ func (a *Application) addHeaders(headers http.Header, c *Claims) {
headers.Set("X-authentik-jwt", c.RawToken)
// System headers
headers.Set("X-authentik-meta-jwks", a.proxyConfig.OidcConfiguration.JwksUri)
headers.Set("X-authentik-meta-jwks", a.endpint.JwksUri)
headers.Set("X-authentik-meta-outpost", a.outpostName)
headers.Set("X-authentik-meta-provider", a.proxyConfig.Name)
headers.Set("X-authentik-meta-app", a.proxyConfig.AssignedApplicationSlug)
@ -65,7 +59,8 @@ func (a *Application) addHeaders(headers http.Header, c *Claims) {
}
}
func (a *Application) getTraefikForwardUrl(r *http.Request) *url.URL {
// getTraefikForwardUrl See https://doc.traefik.io/traefik/middlewares/forwardauth/
func (a *Application) getTraefikForwardUrl(r *http.Request) (*url.URL, error) {
u, err := url.Parse(fmt.Sprintf(
"%s://%s%s",
r.Header.Get("X-Forwarded-Proto"),
@ -73,33 +68,63 @@ func (a *Application) getTraefikForwardUrl(r *http.Request) *url.URL {
r.Header.Get("X-Forwarded-Uri"),
))
if err != nil {
a.log.WithError(err).Warning("Failed to parse URL from traefik")
return r.URL
return nil, err
}
a.log.WithField("url", u.String()).Trace("traefik forwarded url")
return u
return u, nil
}
func (a *Application) IsAllowlisted(r *http.Request) bool {
url := r.URL
// In Forward auth mode, we can't directly match against the requested URL
// Since that would be /akprox/auth/...
if a.Mode() == api.PROXYMODE_FORWARD_SINGLE || a.Mode() == api.PROXYMODE_FORWARD_DOMAIN {
// For traefik, we can get the Upstream URL from headers
// For nginx we can attempt to as well, but it's not guaranteed to work.
if strings.HasPrefix(r.URL.Path, "/akprox/auth") {
url = a.getTraefikForwardUrl(r)
// getNginxForwardUrl See https://github.com/kubernetes/ingress-nginx/blob/main/rootfs/etc/nginx/template/nginx.tmpl
func (a *Application) getNginxForwardUrl(r *http.Request) (*url.URL, error) {
ou := r.Header.Get("X-Original-URI")
if ou != "" {
// Turn this full URL into a relative URL
u := &url.URL{
Host: "",
Scheme: "",
Path: ou,
}
a.log.WithField("url", u.String()).Info("building forward URL from X-Original-URI")
return u, nil
}
for _, u := range a.UnauthenticatedRegex {
h := r.Header.Get("X-Original-URL")
if len(h) < 1 {
return nil, errors.New("no forward URL found")
}
u, err := url.Parse(h)
if err != nil {
a.log.WithError(err).Warning("failed to parse URL from nginx")
return nil, err
}
a.log.WithField("url", u.String()).Trace("nginx forwarded url")
return u, nil
}
func (a *Application) ReportMisconfiguration(r *http.Request, msg string, fields map[string]interface{}) {
fields["message"] = msg
a.log.WithFields(fields).Error("Reporting configuration error")
req := api.EventRequest{
Action: api.EVENTACTIONS_CONFIGURATION_ERROR,
App: "authentik.providers.proxy", // must match python apps.py name
ClientIp: *api.NewNullableString(api.PtrString(r.RemoteAddr)),
Context: &fields,
}
_, _, err := a.ak.Client.EventsApi.EventsEventsCreate(context.Background()).EventRequest(req).Execute()
if err != nil {
a.log.WithError(err).Warning("failed to report configuration error")
}
}
func (a *Application) IsAllowlisted(u *url.URL) bool {
for _, ur := range a.UnauthenticatedRegex {
var testString string
if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE {
testString = url.Path
testString = u.Path
} else {
testString = url.String()
testString = u.String()
}
a.log.WithField("regex", u.String()).WithField("url", testString).Trace("Matching URL against allow list")
if u.MatchString(testString) {
if ur.MatchString(testString) {
return true
}
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"net/url"
"strings"
"goauthentik.io/api"
"goauthentik.io/internal/outpost/proxyv2/constants"
@ -24,39 +25,52 @@ func (a *Application) configureForward() error {
}
func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Request) {
a.log.WithField("header", r.Header).Trace("tracing headers for debug")
// First check if we've got everything we need
fwd, err := a.getTraefikForwardUrl(r)
if err != nil {
a.ReportMisconfiguration(r, fmt.Sprintf("Outpost %s (Provider %s) failed to detect a forward URL from Traefik", a.outpostName, a.proxyConfig.Name), map[string]interface{}{
"provider": a.proxyConfig.Name,
"outpost": a.outpostName,
"url": r.URL.String(),
"headers": cleanseHeaders(r.Header),
})
http.Error(rw, "configuration error", http.StatusInternalServerError)
return
}
claims, err := a.getClaims(r)
if claims != nil && err == nil {
a.addHeaders(rw.Header(), claims)
rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth")
return
} else if claims == nil && a.IsAllowlisted(r) {
} else if claims == nil && a.IsAllowlisted(fwd) {
a.log.Trace("path can be accessed without authentication")
return
}
if strings.HasPrefix(r.Header.Get("X-Forwarded-Uri"), "/akprox") {
a.log.WithField("url", r.URL.String()).Trace("path begins with /akprox, allowing access")
return
}
host := ""
s, _ := a.sessions.Get(r, constants.SeesionName)
// Optional suffix, which is appended to the URL
if *a.proxyConfig.Mode == api.PROXYMODE_FORWARD_SINGLE {
host = web.GetHost(r)
} else if *a.proxyConfig.Mode == api.PROXYMODE_FORWARD_DOMAIN {
eh, _ := url.Parse(a.proxyConfig.ExternalHost)
host = eh.Host
eh, err := url.Parse(a.proxyConfig.ExternalHost)
if err != nil {
a.log.WithField("host", a.proxyConfig.ExternalHost).WithError(err).Warning("invalid external_host")
} else {
host = eh.Host
}
}
// set the redirect flag to the current URL we have, since we redirect
// to a (possibly) different domain, but we want to be redirected back
// to the application
// see https://doc.traefik.io/traefik/middlewares/forwardauth/
// X-Forwarded-Uri is only the path, so we need to build the entire URL
s.Values[constants.SessionRedirect] = a.getTraefikForwardUrl(r).String()
if r.Header.Get("X-Forwarded-Uri") == "/akprox/start" {
a.log.Info("Detected potential redirect loop")
if val, ok := s.Values[constants.SessionLoopDetection]; !ok {
s.Values[constants.SessionLoopDetection] = 1
} else {
s.Values[constants.SessionLoopDetection] = val.(int) + 1
}
}
s.Values[constants.SessionRedirect] = fwd.String()
err = s.Save(r, rw)
if err != nil {
a.log.WithError(err).Warning("failed to save session before redirect")
@ -72,6 +86,19 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque
}
func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request) {
a.log.WithField("header", r.Header).Trace("tracing headers for debug")
fwd, err := a.getNginxForwardUrl(r)
if err != nil {
a.ReportMisconfiguration(r, fmt.Sprintf("Outpost %s (Provider %s) failed to detect a forward URL from nginx", a.outpostName, a.proxyConfig.Name), map[string]interface{}{
"provider": a.proxyConfig.Name,
"outpost": a.outpostName,
"url": r.URL.String(),
"headers": cleanseHeaders(r.Header),
})
http.Error(rw, "configuration error", http.StatusInternalServerError)
return
}
claims, err := a.getClaims(r)
if claims != nil && err == nil {
a.addHeaders(rw.Header(), claims)
@ -79,9 +106,23 @@ func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request
rw.WriteHeader(200)
a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth")
return
} else if claims == nil && a.IsAllowlisted(r) {
} else if claims == nil && a.IsAllowlisted(fwd) {
a.log.Trace("path can be accessed without authentication")
return
}
s, _ := a.sessions.Get(r, constants.SeesionName)
s.Values[constants.SessionRedirect] = fwd.String()
err = s.Save(r, rw)
if err != nil {
a.log.WithError(err).Warning("failed to save session before redirect")
}
if fwd.String() != r.URL.String() {
if strings.HasPrefix(fwd.Path, "/akprox") {
a.log.WithField("url", r.URL.String()).Trace("path begins with /akprox, allowing access")
return
}
}
http.Error(rw, "unauthorized request", http.StatusUnauthorized)
}

View File

@ -0,0 +1,134 @@
package application
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"goauthentik.io/api"
"goauthentik.io/internal/outpost/proxyv2/constants"
)
func TestForwardHandleNginx_Single_Blank(t *testing.T) {
a := newTestApplication()
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
rr := httptest.NewRecorder()
a.forwardHandleNginx(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
}
func TestForwardHandleNginx_Single_Skip(t *testing.T) {
a := newTestApplication()
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
req.Header.Set("X-Original-URL", "http://test.goauthentik.io/skip")
rr := httptest.NewRecorder()
a.forwardHandleNginx(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestForwardHandleNginx_Single_Headers(t *testing.T) {
a := newTestApplication()
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
req.Header.Set("X-Original-URL", "http://test.goauthentik.io/app")
rr := httptest.NewRecorder()
a.forwardHandleNginx(rr, req)
assert.Equal(t, rr.Code, http.StatusUnauthorized)
s, _ := a.sessions.Get(req, constants.SeesionName)
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
}
func TestForwardHandleNginx_Single_URI(t *testing.T) {
a := newTestApplication()
req, _ := http.NewRequest("GET", "https://foo.bar/akprox/auth/nginx", nil)
req.Header.Set("X-Original-URI", "/app")
rr := httptest.NewRecorder()
a.forwardHandleNginx(rr, req)
assert.Equal(t, rr.Code, http.StatusUnauthorized)
s, _ := a.sessions.Get(req, constants.SeesionName)
assert.Equal(t, "/app", s.Values[constants.SessionRedirect])
}
func TestForwardHandleNginx_Single_Claims(t *testing.T) {
a := newTestApplication()
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
req.Header.Set("X-Original-URI", "/")
rr := httptest.NewRecorder()
a.forwardHandleNginx(rr, req)
s, _ := a.sessions.Get(req, constants.SeesionName)
s.Values[constants.SessionClaims] = Claims{
Sub: "foo",
Proxy: ProxyClaims{
UserAttributes: map[string]interface{}{
"username": "foo",
"password": "bar",
"additionalHeaders": map[string]interface{}{
"foo": "bar",
},
},
},
}
err := a.sessions.Save(req, rr, s)
if err != nil {
panic(err)
}
rr = httptest.NewRecorder()
a.forwardHandleNginx(rr, req)
h := rr.Result().Header
assert.Equal(t, []string{"Basic Zm9vOmJhcg=="}, h["Authorization"])
assert.Equal(t, []string{"bar"}, h["Foo"])
assert.Equal(t, []string{""}, h["User-Agent"])
assert.Equal(t, []string{""}, h["X-Authentik-Email"])
assert.Equal(t, []string{""}, h["X-Authentik-Groups"])
assert.Equal(t, []string{""}, h["X-Authentik-Jwt"])
assert.Equal(t, []string{""}, h["X-Authentik-Meta-App"])
assert.Equal(t, []string{""}, h["X-Authentik-Meta-Jwks"])
assert.Equal(t, []string{""}, h["X-Authentik-Meta-Outpost"])
assert.Equal(t, []string{""}, h["X-Authentik-Name"])
assert.Equal(t, []string{"foo"}, h["X-Authentik-Uid"])
assert.Equal(t, []string{""}, h["X-Authentik-Username"])
}
func TestForwardHandleNginx_Domain_Blank(t *testing.T) {
a := newTestApplication()
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
a.proxyConfig.CookieDomain = api.PtrString("foo")
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
rr := httptest.NewRecorder()
a.forwardHandleNginx(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
}
func TestForwardHandleNginx_Domain_Header(t *testing.T) {
a := newTestApplication()
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
a.proxyConfig.CookieDomain = api.PtrString("foo")
a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io"
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
req.Header.Set("X-Original-URL", "http://test.goauthentik.io/app")
rr := httptest.NewRecorder()
a.forwardHandleNginx(rr, req)
assert.Equal(t, http.StatusUnauthorized, rr.Code)
s, _ := a.sessions.Get(req, constants.SeesionName)
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
}

View File

@ -0,0 +1,132 @@
package application
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"goauthentik.io/api"
"goauthentik.io/internal/outpost/proxyv2/constants"
)
func TestForwardHandleTraefik_Single_Blank(t *testing.T) {
a := newTestApplication()
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
rr := httptest.NewRecorder()
a.forwardHandleTraefik(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
}
func TestForwardHandleTraefik_Single_Skip(t *testing.T) {
a := newTestApplication()
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
req.Header.Set("X-Forwarded-Proto", "http")
req.Header.Set("X-Forwarded-Host", "test.goauthentik.io")
req.Header.Set("X-Forwarded-Uri", "/skip")
rr := httptest.NewRecorder()
a.forwardHandleTraefik(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestForwardHandleTraefik_Single_Headers(t *testing.T) {
a := newTestApplication()
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
req.Header.Set("X-Forwarded-Proto", "http")
req.Header.Set("X-Forwarded-Host", "test.goauthentik.io")
req.Header.Set("X-Forwarded-Uri", "/app")
rr := httptest.NewRecorder()
a.forwardHandleTraefik(rr, req)
assert.Equal(t, rr.Code, http.StatusTemporaryRedirect)
loc, _ := rr.Result().Location()
assert.Equal(t, loc.String(), "http://test.goauthentik.io/akprox/start")
s, _ := a.sessions.Get(req, constants.SeesionName)
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
}
func TestForwardHandleTraefik_Single_Claims(t *testing.T) {
a := newTestApplication()
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
req.Header.Set("X-Forwarded-Proto", "http")
req.Header.Set("X-Forwarded-Host", "test.goauthentik.io")
req.Header.Set("X-Forwarded-Uri", "/app")
rr := httptest.NewRecorder()
a.forwardHandleTraefik(rr, req)
s, _ := a.sessions.Get(req, constants.SeesionName)
s.Values[constants.SessionClaims] = Claims{
Sub: "foo",
Proxy: ProxyClaims{
UserAttributes: map[string]interface{}{
"username": "foo",
"password": "bar",
"additionalHeaders": map[string]interface{}{
"foo": "bar",
},
},
},
}
err := a.sessions.Save(req, rr, s)
if err != nil {
panic(err)
}
rr = httptest.NewRecorder()
a.forwardHandleTraefik(rr, req)
h := rr.Result().Header
assert.Equal(t, []string{"Basic Zm9vOmJhcg=="}, h["Authorization"])
assert.Equal(t, []string{"bar"}, h["Foo"])
assert.Equal(t, []string{""}, h["User-Agent"])
assert.Equal(t, []string{""}, h["X-Authentik-Email"])
assert.Equal(t, []string{""}, h["X-Authentik-Groups"])
assert.Equal(t, []string{""}, h["X-Authentik-Jwt"])
assert.Equal(t, []string{""}, h["X-Authentik-Meta-App"])
assert.Equal(t, []string{""}, h["X-Authentik-Meta-Jwks"])
assert.Equal(t, []string{""}, h["X-Authentik-Meta-Outpost"])
assert.Equal(t, []string{""}, h["X-Authentik-Name"])
assert.Equal(t, []string{"foo"}, h["X-Authentik-Uid"])
assert.Equal(t, []string{""}, h["X-Authentik-Username"])
}
func TestForwardHandleTraefik_Domain_Blank(t *testing.T) {
a := newTestApplication()
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
a.proxyConfig.CookieDomain = api.PtrString("foo")
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
rr := httptest.NewRecorder()
a.forwardHandleTraefik(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
}
func TestForwardHandleTraefik_Domain_Header(t *testing.T) {
a := newTestApplication()
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
a.proxyConfig.CookieDomain = api.PtrString("foo")
a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io"
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
req.Header.Set("X-Forwarded-Proto", "http")
req.Header.Set("X-Forwarded-Host", "test.goauthentik.io")
req.Header.Set("X-Forwarded-Uri", "/app")
rr := httptest.NewRecorder()
a.forwardHandleTraefik(rr, req)
assert.Equal(t, http.StatusTemporaryRedirect, rr.Code)
loc, _ := rr.Result().Location()
assert.Equal(t, "http://auth.test.goauthentik.io/akprox/start", loc.String())
s, _ := a.sessions.Get(req, constants.SeesionName)
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
}

View File

@ -35,7 +35,7 @@ func (a *Application) configureProxy() error {
rp.ModifyResponse = a.proxyModifyResponse
a.mux.PathPrefix("/").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
claims, err := a.getClaims(r)
if claims == nil && a.IsAllowlisted(r) {
if claims == nil && a.IsAllowlisted(r.URL) {
a.log.Trace("path can be accessed without authentication")
} else if claims == nil && err != nil {
a.redirectToStart(rw, r)
@ -60,7 +60,7 @@ func (a *Application) configureProxy() error {
}
metrics.UpstreamTiming.With(prometheus.Labels{
"outpost_name": a.outpostName,
"upstream_host": u.String(),
"upstream_host": r.URL.Host,
"scheme": r.URL.Scheme,
"method": r.Method,
"path": r.URL.Path,
@ -71,13 +71,24 @@ func (a *Application) configureProxy() error {
return nil
}
func (a *Application) proxyModifyRequest(u *url.URL) func(req *http.Request) {
return func(req *http.Request) {
req.URL.Scheme = u.Scheme
req.URL.Host = u.Host
func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) {
return func(r *http.Request) {
claims, _ := a.getClaims(r)
if claims.Proxy.BackendOverride != "" {
u, err := url.Parse(claims.Proxy.BackendOverride)
if err != nil {
a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override")
}
r.URL.Scheme = u.Scheme
r.URL.Host = u.Host
} else {
r.URL.Scheme = ou.Scheme
r.URL.Host = ou.Host
}
}
}
func (a *Application) proxyModifyResponse(res *http.Response) error {
res.Header.Set("X-Powered-By", "authentik_proxy2")
return nil
}

View File

@ -25,13 +25,6 @@ func (a *Application) handleRedirect(rw http.ResponseWriter, r *http.Request) {
if err != nil {
a.log.WithError(err).Warning("failed to save session")
}
if loop, ok := s.Values[constants.SessionLoopDetection]; ok {
if loop.(int) > 10 {
rw.WriteHeader(http.StatusBadRequest)
a.ErrorPage(rw, r, "Detected redirect loop, make sure /akprox is accessible without authentication.")
return
}
}
http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(newState), http.StatusFound)
}

View File

@ -19,7 +19,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig) sessions.Store {
if err != nil {
panic(err)
}
rs.SetMaxLength(math.MaxInt64)
rs.SetMaxLength(math.MaxInt)
if p.TokenValidity.IsSet() {
t := p.TokenValidity.Get()
// Add one to the validity to ensure we don't have a session with indefinite length
@ -39,7 +39,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig) sessions.Store {
// when using OpenID Connect , since this can contain a large amount of extra information in the id_token
// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
cs.MaxLength(math.MaxInt64)
cs.MaxLength(math.MaxInt)
if p.TokenValidity.IsSet() {
t := p.TokenValidity.Get()
// Add one to the validity to ensure we don't have a session with indefinite length

View File

@ -0,0 +1,40 @@
package application
import (
"net/http"
"github.com/quasoft/memstore"
"goauthentik.io/api"
"goauthentik.io/internal/outpost/ak"
)
func newTestApplication() *Application {
a, _ := NewApplication(
api.ProxyOutpostConfig{
Name: ak.TestSecret(),
ClientId: api.PtrString(ak.TestSecret()),
ClientSecret: api.PtrString(ak.TestSecret()),
CookieSecret: api.PtrString(ak.TestSecret()),
CookieDomain: api.PtrString(""),
Mode: api.PROXYMODE_FORWARD_SINGLE.Ptr(),
SkipPathRegex: api.PtrString("/skip.*"),
BasicAuthEnabled: api.PtrBool(true),
BasicAuthUserAttribute: api.PtrString("username"),
BasicAuthPasswordAttribute: api.PtrString("password"),
},
http.DefaultClient,
nil,
ak.MockAK(
api.Outpost{
Config: map[string]interface{}{
"authentik_host": ak.TestSecret(),
},
},
ak.MockConfig(),
),
)
a.sessions = memstore.NewMemStore(
[]byte(ak.TestSecret()),
)
return a
}

View File

@ -6,7 +6,9 @@ import (
"net/url"
"path"
"strconv"
"strings"
"goauthentik.io/api"
"goauthentik.io/internal/outpost/proxyv2/constants"
)
@ -20,6 +22,26 @@ func urlJoin(originalUrl string, newPath string) string {
}
func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) {
s, err := a.sessions.Get(r, constants.SeesionName)
if err == nil {
a.log.WithError(err).Warning("failed to decode session")
}
redirectUrl := urlJoin(a.proxyConfig.ExternalHost, r.URL.Path)
if a.Mode() == api.PROXYMODE_FORWARD_DOMAIN {
dom := strings.TrimPrefix(*a.proxyConfig.CookieDomain, ".")
// In forward_domain we only check that the current URL's host
// ends with the cookie domain (remove the leading period if set)
if !strings.HasSuffix(r.URL.Hostname(), dom) {
a.log.WithField("url", r.URL.String()).WithField("cd", dom).Warning("Invalid redirect found")
redirectUrl = a.proxyConfig.ExternalHost
}
}
s.Values[constants.SessionRedirect] = redirectUrl
err = s.Save(r, rw)
if err != nil {
a.log.WithError(err).Warning("failed to save session before redirect")
}
authUrl := urlJoin(a.proxyConfig.ExternalHost, "/akprox/start")
http.Redirect(rw, r, authUrl, http.StatusFound)
}
@ -65,3 +87,13 @@ func contains(s []string, e string) bool {
}
return false
}
func cleanseHeaders(headers http.Header) map[string]string {
h := make(map[string]string)
for hk, hv := range headers {
if len(hv) > 0 {
h[hk] = hv[0]
}
}
return h
}

View File

@ -0,0 +1,81 @@
package application
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"goauthentik.io/api"
"goauthentik.io/internal/outpost/proxyv2/constants"
)
func TestRedirectToStart_Proxy(t *testing.T) {
a := newTestApplication()
a.proxyConfig.Mode = api.PROXYMODE_PROXY.Ptr()
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
req, _ := http.NewRequest("GET", "/foo/bar/baz", nil)
rr := httptest.NewRecorder()
a.redirectToStart(rr, req)
assert.Equal(t, http.StatusFound, rr.Code)
loc, _ := rr.Result().Location()
assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String())
s, _ := a.sessions.Get(req, constants.SeesionName)
assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect])
}
func TestRedirectToStart_Forward(t *testing.T) {
a := newTestApplication()
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_SINGLE.Ptr()
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
req, _ := http.NewRequest("GET", "/foo/bar/baz", nil)
rr := httptest.NewRecorder()
a.redirectToStart(rr, req)
assert.Equal(t, http.StatusFound, rr.Code)
loc, _ := rr.Result().Location()
assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String())
s, _ := a.sessions.Get(req, constants.SeesionName)
assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect])
}
func TestRedirectToStart_Forward_Domain_Invalid(t *testing.T) {
a := newTestApplication()
a.proxyConfig.CookieDomain = api.PtrString("foo")
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
req, _ := http.NewRequest("GET", "/foo/bar/baz", nil)
rr := httptest.NewRecorder()
a.redirectToStart(rr, req)
assert.Equal(t, http.StatusFound, rr.Code)
loc, _ := rr.Result().Location()
assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String())
s, _ := a.sessions.Get(req, constants.SeesionName)
assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect])
}
func TestRedirectToStart_Forward_Domain(t *testing.T) {
a := newTestApplication()
a.proxyConfig.CookieDomain = api.PtrString("goauthentik.io")
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
req, _ := http.NewRequest("GET", "/foo/bar/baz", nil)
rr := httptest.NewRecorder()
a.redirectToStart(rr, req)
assert.Equal(t, http.StatusFound, rr.Code)
loc, _ := rr.Result().Location()
assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String())
s, _ := a.sessions.Get(req, constants.SeesionName)
assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect])
}

View File

@ -6,4 +6,3 @@ const SessionOAuthState = "oauth_state"
const SessionClaims = "claims"
const SessionRedirect = "redirect"
const SessionLoopDetection = "loop_detection"

View File

@ -8,6 +8,8 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
"goauthentik.io/api"
"goauthentik.io/internal/outpost/proxyv2/application"
"goauthentik.io/internal/outpost/proxyv2/metrics"
"goauthentik.io/internal/utils/web"
staticWeb "goauthentik.io/web"
@ -43,6 +45,50 @@ func (ps *ProxyServer) HandleStatic(rw http.ResponseWriter, r *http.Request) {
}).Observe(float64(after))
}
func (ps *ProxyServer) lookupApp(r *http.Request) (*application.Application, string) {
host := web.GetHost(r)
// Try to find application by directly looking up host first (proxy, forward_auth_single)
a, ok := ps.apps[host]
if ok {
ps.log.WithField("host", host).WithField("app", a.ProxyConfig().Name).Debug("Found app based direct host match")
return a, host
}
// For forward_auth_domain, we don't have a direct app to domain relationship
// Check through all apps, and check how much of their cookie domain matches the host
// Return the application that has the longest match
var longestMatch *application.Application
longestMatchLength := 0
for _, app := range ps.apps {
if app.Mode() != api.PROXYMODE_FORWARD_DOMAIN {
continue
}
// Check if the cookie domain has a leading period for a wildcard
// This will decrease the weight of a wildcard domain, but a request to example.com
// with the cookie domain set to example.com will still be routed correctly.
cd := strings.TrimPrefix(*app.ProxyConfig().CookieDomain, ".")
if !strings.HasSuffix(host, cd) {
continue
}
if len(cd) < longestMatchLength {
continue
}
longestMatch = app
longestMatchLength = len(cd)
// Also for forward_auth_domain, we need to respond on the external domain
if app.ProxyConfig().ExternalHost == host {
ps.log.WithField("host", host).WithField("app", app.ProxyConfig().Name).Debug("Found app based on external_host")
return app, host
}
}
// Check if our longes match is 0, in which case we didn't match, so we
// manually return no app
if longestMatchLength == 0 {
return nil, host
}
ps.log.WithField("host", host).WithField("app", longestMatch.ProxyConfig().Name).Debug("Found app based on cookie domain")
return longestMatch, host
}
func (ps *ProxyServer) Handle(rw http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/akprox/static") {
ps.HandleStatic(rw, r)
@ -52,9 +98,8 @@ func (ps *ProxyServer) Handle(rw http.ResponseWriter, r *http.Request) {
ps.HandlePing(rw, r)
return
}
host := web.GetHost(r)
a, ok := ps.apps[host]
if !ok {
a, host := ps.lookupApp(r)
if a == nil {
// If we only have one handler, host name switching doesn't matter
if len(ps.apps) == 1 {
ps.log.WithField("host", host).Trace("passing to single app mux")

View File

@ -46,7 +46,7 @@ func NewProxyServer(ac *ak.APIController, portOffset int) *ProxyServer {
rootMux.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
h.ServeHTTP(rw, r)
rw.Header().Set("Server", "authentik_proxy2")
rw.Header().Set("X-Powered-By", "authentik_proxy2")
})
})
@ -70,11 +70,12 @@ func NewProxyServer(ac *ak.APIController, portOffset int) *ProxyServer {
return s
}
func (ps *ProxyServer) HandleHost(host string, rw http.ResponseWriter, r *http.Request) bool {
if app, ok := ps.apps[host]; ok {
if app.Mode() == api.PROXYMODE_PROXY {
func (ps *ProxyServer) HandleHost(rw http.ResponseWriter, r *http.Request) bool {
a, host := ps.lookupApp(r)
if a != nil {
if a.Mode() == api.PROXYMODE_PROXY {
ps.log.WithField("host", host).Trace("routing to proxy outpost")
app.ServeHTTP(rw, r)
a.ServeHTTP(rw, r)
return true
}
}
@ -90,7 +91,7 @@ func (ps *ProxyServer) TimerFlowCacheExpiry() {}
func (ps *ProxyServer) GetCertificate(serverName string) *tls.Certificate {
app, ok := ps.apps[serverName]
if !ok {
ps.log.WithField("server-name", serverName).Debug("app does not exist")
ps.log.WithField("server-name", serverName).Debug("failed to get certificate for ServerName")
return nil
}
if app.Cert == nil {
@ -150,17 +151,14 @@ func (ps *ProxyServer) Start() error {
wg.Add(3)
go func() {
defer wg.Done()
ps.log.Debug("Starting HTTP Server...")
ps.ServeHTTP()
}()
go func() {
defer wg.Done()
ps.log.Debug("Starting HTTPs Server...")
ps.ServeHTTPS()
}()
go func() {
defer wg.Done()
ps.log.Debug("Starting Metrics Server...")
metrics.RunServer()
}()
return nil

View File

@ -99,14 +99,14 @@ func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
h.handler.ServeHTTP(responseLogger, req)
duration := float64(time.Since(t)) / float64(time.Millisecond)
h.afterHandler(h.logger.WithFields(log.Fields{
"remote": req.RemoteAddr,
"host": GetHost(req),
"request_protocol": req.Proto,
"runtime": fmt.Sprintf("%0.3f", duration),
"method": req.Method,
"size": responseLogger.Size(),
"status": responseLogger.Status(),
"upstream": responseLogger.upstream,
"request_useragent": req.UserAgent(),
"remote": req.RemoteAddr,
"host": GetHost(req),
"runtime": fmt.Sprintf("%0.3f", duration),
"method": req.Method,
"scheme": req.URL.Scheme,
"size": responseLogger.Size(),
"status": responseLogger.Status(),
"upstream": responseLogger.upstream,
"user_agent": req.UserAgent(),
}), req).Info(url.RequestURI())
}

View File

@ -47,10 +47,9 @@ func (ws *WebServer) configureProxy() {
ws.proxyErrorHandler(rw, r, fmt.Errorf("authentik core not running yet"))
return
}
host := web.GetHost(r)
before := time.Now()
if ws.ProxyServer != nil {
if ws.ProxyServer.HandleHost(host, rw, r) {
if ws.ProxyServer.HandleHost(rw, r) {
Requests.With(prometheus.Labels{
"dest": "embedded_outpost",
}).Observe(float64(time.Since(before)))
@ -60,7 +59,7 @@ func (ws *WebServer) configureProxy() {
Requests.With(prometheus.Labels{
"dest": "py",
}).Observe(float64(time.Since(before)))
ws.log.WithField("host", host).Trace("routing to application server")
ws.log.WithField("host", web.GetHost(r)).Trace("routing to application server")
rp.ServeHTTP(rw, r)
})
}
@ -75,6 +74,6 @@ func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request
}
func (ws *WebServer) proxyModifyResponse(r *http.Response) error {
r.Header.Set("server", "authentik")
r.Header.Set("X-Powered-By", "authentik")
return nil
}

View File

@ -1,5 +1,5 @@
# Stage 1: Build
FROM docker.io/golang:1.17.5-bullseye AS builder
FROM docker.io/golang:1.17.6-bullseye AS builder
WORKDIR /go/src/goauthentik.io

View File

@ -61,11 +61,15 @@ MODE_FILE="/tmp/authentik-mode"
if [[ "$1" == "server" ]]; then
wait_for_db
echo "server" > $MODE_FILE
# We only set prometheus_multiproc_dir for serer, as with the worker it just fills up the disk
export prometheus_multiproc_dir=/dev/shm/
# We only set PROMETHEUS_MULTIPROC_DIR for serer, as with the worker it just fills up the disk
# as one file is created per process
#
# Set to TMPDIR instead hardcoded path so this can be used outside docker too
export PROMETHEUS_MULTIPROC_DIR=$TMPDIR
python -m lifecycle.migrate
/authentik-proxy
elif [[ "$1" == "worker" ]]; then
wait_for_db
echo "worker" > $MODE_FILE
check_if_root "celery -A authentik.root.celery worker -Ofair --max-tasks-per-child=1 --autoscale 3,1 -E -B -s /tmp/celerybeat-schedule -Q authentik,authentik_scheduled,authentik_events"
elif [[ "$1" == "flower" ]]; then

View File

@ -1,13 +1,18 @@
"""Gunicorn config"""
import os
import pwd
from hashlib import sha512
from multiprocessing import cpu_count
import structlog
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
from authentik import get_full_version
from authentik.lib.config import CONFIG
from authentik.lib.utils.http import get_http_session
from authentik.lib.utils.reflection import get_env
bind = "127.0.0.1:8000"
reload = True
try:
pwd.getpwnam("authentik")
@ -23,6 +28,9 @@ if os.path.exists("/dev/shm"): # nosec
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
max_requests = 1000
max_requests_jitter = 50
logconfig_dict = {
"version": 1,
"disable_existing_loggers": False,
@ -60,3 +68,38 @@ else:
workers = int(os.environ.get("WORKERS", default_workers))
threads = int(os.environ.get("THREADS", 4))
# pylint: disable=unused-argument
def worker_exit(server, worker):
"""Remove pid dbs when worker is shutdown"""
from prometheus_client import multiprocess
multiprocess.mark_process_dead(worker.pid)
if not CONFIG.y_bool("disable_startup_analytics", False):
env = get_env()
should_send = env not in ["dev", "ci"]
if should_send:
try:
get_http_session().post(
"https://goauthentik.io/api/event",
json={
"domain": "authentik",
"name": "pageview",
"referrer": get_full_version(),
"url": (
f"http://localhost/{env}?utm_source={get_full_version()}&utm_medium={env}"
),
},
headers={
"User-Agent": sha512(str(CONFIG.y("secret_key")).encode("ascii")).hexdigest()[
:16
],
"Content-Type": "application/json",
},
timeout=5,
)
# pylint: disable=bare-except
except: # nosec
pass

View File

@ -2,6 +2,7 @@
"""This file needs to be run from the root of the project to correctly
import authentik. This is done by the dockerfile."""
from json import dumps
from sys import exit as sysexit
from sys import stderr
from time import sleep, time
@ -25,6 +26,16 @@ def j_print(event: str, log_level: str = "info", **kwargs):
print(dumps(data), file=stderr)
j_print("Starting authentik bootstrap")
# Sanity check, ensure SECRET_KEY is set before we even check for database connectivity
if CONFIG.y("secret_key") is None or len(CONFIG.y("secret_key")) == 0:
j_print("----------------------------------------------------------------------")
j_print("Secret key missing, check https://goauthentik.io/docs/installation/.")
j_print("----------------------------------------------------------------------")
sysexit(1)
while True:
try:
conn = connect(
@ -38,7 +49,9 @@ while True:
break
except OperationalError as exc:
sleep(1)
j_print(f"PostgreSQL Connection failed, retrying... ({exc})")
j_print(f"PostgreSQL connection failed, retrying... ({exc})")
finally:
j_print("PostgreSQL connection successful")
REDIS_PROTOCOL_PREFIX = "redis://"
if CONFIG.y_bool("redis.tls", False):
@ -56,3 +69,7 @@ while True:
except RedisError as exc:
sleep(1)
j_print(f"Redis Connection failed, retrying... ({exc})", redis_url=REDIS_URL)
finally:
j_print("Redis Connection successful")
j_print("Finished authentik bootstrap")

Binary file not shown.

File diff suppressed because it is too large Load Diff

738
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ ENV NODE_ENV=production
RUN cd /static && npm i && npm run build-proxy
# Stage 2: Build
FROM docker.io/golang:1.17.5-bullseye AS builder
FROM docker.io/golang:1.17.6-bullseye AS builder
WORKDIR /go/src/goauthentik.io

View File

@ -14,7 +14,7 @@ pythonPlatform = "Linux"
[tool.black]
line-length = 100
target-version = ['py39']
target-version = ['py310']
exclude = 'node_modules'
[tool.isort]
@ -92,7 +92,7 @@ addopts = "-p no:celery --junitxml=unittest.xml"
[tool.poetry]
name = "authentik"
version = "2021.12.5"
version = "2022.1.4"
description = ""
authors = ["Jens Langhammer <jens.langhammer@beryju.org>"]
@ -135,7 +135,7 @@ pyjwt = "*"
python = "^3.10"
pyyaml = "*"
requests-oauthlib = "*"
sentry-sdk = { git = 'https://github.com/beryju/sentry-python.git', rev = '379aee28b15d3b87b381317746c4efd24b3d7bc3' }
sentry-sdk = "*"
service_identity = "*"
structlog = "*"
swagger-spec-validator = "*"

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2021.12.5
version: 2022.1.4
description: Making authentication simple.
contact:
email: hello@beryju.org
@ -15270,6 +15270,14 @@ paths:
operationId: stages_authenticator_webauthn_list
description: AuthenticateWebAuthnStage Viewset
parameters:
- in: query
name: authenticator_attachment
schema:
type: string
nullable: true
enum:
- cross-platform
- platform
- in: query
name: configure_flow
schema:
@ -15297,6 +15305,14 @@ paths:
description: Number of results to return per page.
schema:
type: integer
- in: query
name: resident_key_requirement
schema:
type: string
enum:
- discouraged
- preferred
- required
- name: search
required: false
in: query
@ -19174,6 +19190,12 @@ components:
If empty, user will not be able to configure this stage.
user_verification:
$ref: '#/components/schemas/UserVerificationEnum'
authenticator_attachment:
allOf:
- $ref: '#/components/schemas/AuthenticatorAttachmentEnum'
nullable: true
resident_key_requirement:
$ref: '#/components/schemas/ResidentKeyRequirementEnum'
required:
- component
- meta_model_name
@ -19200,6 +19222,12 @@ components:
If empty, user will not be able to configure this stage.
user_verification:
$ref: '#/components/schemas/UserVerificationEnum'
authenticator_attachment:
allOf:
- $ref: '#/components/schemas/AuthenticatorAttachmentEnum'
nullable: true
resident_key_requirement:
$ref: '#/components/schemas/ResidentKeyRequirementEnum'
required:
- name
AuthenticatedSession:
@ -19288,6 +19316,11 @@ components:
- last_used
- user
- user_agent
AuthenticatorAttachmentEnum:
enum:
- platform
- cross-platform
type: string
AuthenticatorDuoChallenge:
type: object
description: Duo Challenge
@ -26519,6 +26552,12 @@ components:
If empty, user will not be able to configure this stage.
user_verification:
$ref: '#/components/schemas/UserVerificationEnum'
authenticator_attachment:
allOf:
- $ref: '#/components/schemas/AuthenticatorAttachmentEnum'
nullable: true
resident_key_requirement:
$ref: '#/components/schemas/ResidentKeyRequirementEnum'
PatchedAuthenticatorDuoStageRequest:
type: object
description: AuthenticatorDuoStage Serializer
@ -29376,6 +29415,12 @@ components:
type: integer
maximum: 2147483647
minimum: -2147483648
ResidentKeyRequirementEnum:
enum:
- discouraged
- preferred
- required
type: string
SAMLMetadata:
type: object
description: SAML Provider Metadata serializer
@ -31331,4 +31376,3 @@ components:
name: Authorization
servers:
- url: /api/v3/
- url: /api/v2beta/

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