Compare commits

..

333 Commits

Author SHA1 Message Date
9c42b75567 release: 2021.12.4 2021-12-23 10:32:48 +01:00
e9a477c1eb root: cleanup bumpversion config
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-23 10:32:46 +01:00
fa60655a5d build(deps): bump github.com/getsentry/sentry-go from 0.11.0 to 0.12.0 (#1987) 2021-12-23 09:54:46 +01:00
5d729b4878 build(deps): bump fuse.js from 6.4.6 to 6.5.0 in /web (#1986) 2021-12-23 09:48:27 +01:00
8692f7233f build(deps): bump goauthentik.io/api from 0.2021123.2 to 0.2021123.3 (#1988) 2021-12-23 09:47:56 +01:00
457e17fec3 website/docs: add small let's encrypt docs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-23 00:59:06 +01:00
87e99625e6 internal: update tenant certificates on outpost refresh
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-23 00:38:49 +01:00
6f32eeea43 website/docs: prepare 2021.12.4
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 23:37:04 +01:00
dfcf8b2d40 root: update python dependencies
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 23:36:56 +01:00
846006f2e3 events: create test notification with event with data
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 23:32:29 +01:00
f557b2129f *: fix random typos
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 23:13:18 +01:00
6dc2003e34 providers/oauth2: fix tests validating JWT incorrectly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 23:00:57 +01:00
0149c89003 providers/oauth2: fix invalid assignments in JWKS view
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 22:41:28 +01:00
f458cae954 providers/proxy: add error handing when field is already gone
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 22:31:53 +01:00
f01d117ce6 providers/proxy: fix imports in migrations
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 22:25:02 +01:00
2bde43e5dc crypto: use older syntax for type union
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 22:22:45 +01:00
84cc0b5490 web: Update Web API Client version (#1984)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 22:16:44 +01:00
2f3026084e providers/oauth2: remove jwt_alg field and set algorithm based on selected keypair, select HS256 when no keypair is selected
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 22:09:49 +01:00
89696edbee website/integrations: cleanup
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 21:46:46 +01:00
c1f0833c09 crypto: improve support for non-rsa private keys (discovery)
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 21:46:22 +01:00
c77f804b77 web/user: fix user details not rendering when loading to a different user settings tab and then switching
closes #1664

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 20:13:52 +01:00
8e83209631 stages/authenticator_validate: fix lint error
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 18:14:35 +01:00
2e48e0cc2f stages/authenticator_validate: fix prompt not triggering when using in non-authentication context
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 18:03:02 +01:00
e72f0ab160 stages/authenticator_validation: refuse passwordless flow if flow is not for authentication
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 18:02:43 +01:00
a3c681cc44 website/docs: cleanup old image names
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 17:38:09 +01:00
5b3a9e29fb stages/authenticator_validate: add passwordless login
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 17:34:46 +01:00
15803dc67d website/docs: revert traefik to not use header regex
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 15:30:01 +01:00
ff37e064c9 build(deps): bump goauthentik.io/api from 0.2021123.1 to 0.2021123.2 (#1983)
Bumps [goauthentik.io/api](https://github.com/goauthentik/client-go) from 0.2021123.1 to 0.2021123.2.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v0.2021123.1...v0.2021123.2)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-22 11:58:06 +01:00
ef8e922e2a web: Update Web API Client version (#1982)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-22 11:51:15 +01:00
34b11524f1 tenants: add web certificate field, make authentik's core certificate configurable based on keypair
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 11:43:45 +01:00
9e2492be5c web/elements: fix link from notification drawer not working in user interface
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 10:49:10 +01:00
b3ba083ff0 internal: cleanup logging, remove duplicate code
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 10:33:21 +01:00
22a8603892 internal: add custom proxy certificates support to embedded outpost
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 10:16:01 +01:00
d83d058a4b build(deps): bump @docusaurus/plugin-client-redirects in /website (#1980) 2021-12-22 09:47:58 +01:00
ec3fd4a3ab build(deps): bump @docusaurus/preset-classic in /website (#1979) 2021-12-22 09:41:01 +01:00
0764668b14 build(deps): bump goauthentik.io/api from 0.2021122.2 to 0.2021123.1 (#1981) 2021-12-22 09:40:00 +01:00
16b6c17305 Revert "policies: don't always clear application cache on post_save"
This reverts commit 5ef385f0bb.

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

# Conflicts:
#	authentik/policies/signals.py
2021-12-22 00:23:19 +01:00
e60509697a web/admin: fix explore integration not opening in new tab
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-22 00:03:28 +01:00
85364af9e9 web: Update Web API Client version (#1978)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-21 21:28:01 +01:00
cf4b4030aa release: 2021.12.3 2021-12-21 20:52:08 +01:00
74dc025869 ci: sentry release even when tests fail
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-21 20:52:03 +01:00
cabdc53553 root: fix compose docker image
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-21 20:51:39 +01:00
29e9f399bd website/docs: prepare 2021.12.3 release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-21 19:50:24 +01:00
dad43017a0 web/admin: use SentryIgnoredError for user errors
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-21 19:44:44 +01:00
7fb939f97b core: fix error when getting launch URL for application with non-existent Provider
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-21 19:40:29 +01:00
Sem
88859b1c26 website/integrations: Updated Gitea Integration (#1972)
* Updated Gitea Integration

Described a fix to a situation where Gitea might require an additional OIDC mapping in order to make the authentication flow function properly.

* Update index.md

Updated as discussed in PR

* Update index.md

Implementing requested changes
2021-12-21 19:39:27 +01:00
c78236a2a2 root: don't set secure cross opener policy
closes #1977

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-21 19:16:22 +01:00
ba55538a34 outposts/proxy: cleanup
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-21 19:16:06 +01:00
f742c73e24 outposts/proxy: fix allowlist for forward_auth
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1970
2021-12-21 15:49:25 +01:00
ca314c262c *: revert to using GHCR directly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-21 13:54:49 +01:00
b932b6c963 website/docs: update log levels
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-21 13:15:17 +01:00
3c048a1921 outposts/proxy: fix session not expiring correctly due to miscalculation
closes #1976

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-21 13:10:57 +01:00
8a60a7e26f providers/proxy: revert to static list of forwarded headers
wildcard is not usable for this since the regular expression doesn't support negative lookahead, meaning we would always forward all headers, including Connection and others

closes #1969

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-21 12:04:54 +01:00
f10b57ba0b outposts/proxy: handle redirect loop in start handler, show error message
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-21 10:07:08 +01:00
e53114a645 build(deps): bump @typescript-eslint/eslint-plugin in /web (#1974)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.7.0 to 5.8.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.8.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>
2021-12-21 09:27:57 +01:00
2e50532518 build(deps): bump codemirror from 5.64.0 to 5.65.0 in /web (#1973) 2021-12-21 09:14:27 +01:00
1936ddfecb build(deps): bump @typescript-eslint/parser from 5.7.0 to 5.8.0 in /web (#1975) 2021-12-21 09:13:50 +01:00
4afef46cb8 ci: improve restore after switching to stable
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 22:47:06 +01:00
92b4244e81 providers/proxy: update traefik regex
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1969
2021-12-20 22:43:58 +01:00
dfbf7027bc providers/proxy: add traefik.ingress.kubernetes.io/router.tls annotation for ingress
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 22:24:42 +01:00
eca2ef20d0 outposts/proxy: add initial redirect-loop prevention
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 22:21:53 +01:00
cac5c7b3ea outposts/proxy: make templates more re-usable
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 22:20:23 +01:00
37ee555c8e outposts/proxy: fix ping URI not being routed
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 22:12:02 +01:00
f910da0f8a outposts: fix initial refresh not calling Server.Refresh()
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 21:47:32 +01:00
fc9d270992 outposts/ldap: fix log formatter and level not being set correctly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 21:46:01 +01:00
dcbc3d788a web/admin: fix border for outpost health status
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 21:40:26 +01:00
4658018a90 Revert "outposts: rename outpost"
This reverts commit a5c30fd9c7.
2021-12-20 21:37:31 +01:00
577b7ee515 providers/proxy: include auth headers
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 21:37:22 +01:00
621773c1ea internal: rework global logging settings, embedded outpost no longer overwrites core, clean up double init
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 21:23:19 +01:00
3da526f20e root: allow trace log level to work for core/embedded
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 21:11:47 +01:00
052e465041 outpost: re-run globalSetup when updating config
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 21:08:03 +01:00
c843f18743 lib: add additional celery logger to sentry ignore
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 21:04:45 +01:00
80d0b14bb8 outposts: fix error when getting state for non-existent outpost
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 19:44:47 +01:00
68637cf7cf outposts: handle/ignore http Abort handler
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 19:42:45 +01:00
82acba26af internal: fix sentry sample rate not applying to proxy
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 19:42:26 +01:00
ff8a812823 web/admin: don't auto-select certificate for LDAP source verification
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 19:31:57 +01:00
7f5fed2aea web/admin: add outpost type to list
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 19:30:52 +01:00
a5c30fd9c7 outposts: rename outpost
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 19:28:05 +01:00
ef23a0da52 outposts/proxy: fix traefik header regex to only match Remote- and X- headers to prevent websocket errors
closes #1969

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 13:30:19 +01:00
ba527e7141 root: drop redis cache sentry errors
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 13:12:14 +01:00
8edc254ab5 root: upgrade python dependencies
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-20 10:45:38 +01:00
42627d21b0 build(deps): bump eslint from 8.4.1 to 8.5.0 in /web (#1966) 2021-12-20 08:48:45 +01:00
2479b157d0 build(deps): bump goauthentik.io/api from 0.2021121.1 to 0.2021122.2 (#1967) 2021-12-20 08:48:22 +01:00
602573f83f ci: fix label
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-19 13:44:34 +01:00
20c33fa011 web: Update Web API Client version (#1962) 2021-12-19 13:31:25 +01:00
8599d9efe0 web/admin: auto set the embedded outpost's authentik_host on first view
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-19 13:27:04 +01:00
8e6fcfe350 root: fix inconsistent URL quoting of redis URLs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 22:24:41 +01:00
558aa45201 web: Update Web API Client version (#1959)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-18 21:37:25 +01:00
e9910732bc release: 2021.12.2 2021-12-18 21:03:50 +01:00
246dd4b062 ci: fix typo
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 21:03:47 +01:00
4425f8d183 ci: only build arm, amd64 on linux and darwin
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 21:02:20 +01:00
c410bb8c36 ci: fix dependency
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 20:50:59 +01:00
44f62a4773 website/docs: add 2021.12.2 release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 20:39:16 +01:00
b6ff04694f providers/oauth2: don't rely on expiry task for access codes and refresh tokens
closes #1911

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 17:42:41 +01:00
d4ce0e8e41 web/elements: fix border between search buttons
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 17:34:28 +01:00
362d72da8c ci: fix shouldBuild being based off of harbor
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 17:01:51 +01:00
88d0f8d8a8 web: Update Web API Client version (#1958)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-18 16:19:18 +01:00
61097b9400 policies/password: add minimum digits
closes #1952

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 16:15:56 +01:00
7a73ddfb60 outposts/proxy: match skipPathRegex against full URL on domain auth
closes #1955

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 15:50:42 +01:00
d66f13c249 web: Update Web API Client version (#1957)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-18 15:38:52 +01:00
8cc3cb6a42 website/docs: fix reference to non-existent version
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 15:34:39 +01:00
4c5537ddfe sources/oauth: allow writing to user in SourceConnection
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1888
2021-12-18 15:33:46 +01:00
a95779157d tests/integration: add rename and full update tests for k8s controller
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 15:32:16 +01:00
70256727fd web: ignore instantSearchSDKJSBridgeClearHighlight error on edge on iOS
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 15:05:40 +01:00
ac6afb2b82 stages/email: add test for non-existent directory
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-18 15:05:40 +01:00
2ea7bd86e8 website/integrations: add Service Provider Binding: Post for rancher (#1956)
Adds a line to the docs that's important for Rancher and Authentik to work
2021-12-18 15:05:27 +01:00
95bce9c9e7 outposts: release binary outposts (#1954)
* outposts/proxy: always embed static assets, still check local

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

* ci: add initial ci to build outpost as binary

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

* ci: fix typo, build web

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

* ci: upload to release on publish, only run linux on ci

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

* ci: ensure latest go is used

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

* ci: split e2e tests into two halves

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-17 19:49:32 +00:00
71a22c2a34 outposts: add unittests for docker controller
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-17 13:42:33 +01:00
f3eb85877d web: Update Web API Client version (#1951)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-17 12:02:22 +01:00
273f5211a0 providers/saml: Fix typo (#1950) 2021-12-17 11:00:20 +00:00
db06428ab9 build(deps): bump goauthentik.io/api from 0.2021104.17 to 0.2021121.1 (#1948)
Bumps [goauthentik.io/api](https://github.com/goauthentik/client-go) from 0.2021104.17 to 0.2021121.1.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v0.2021104.17...v0.2021121.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-17 10:21:58 +01:00
109d8e48d4 website: update integrations categories to match new version
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 21:38:37 +01:00
2ca115285c crypto: fix private keys not being imported correctly
closes #1945

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 21:14:15 +01:00
f5459645a5 web/admin: fix background colour for application sidebar
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 21:06:08 +01:00
14c159500d core: don't rotate non-api tokens
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 19:32:39 +01:00
03da87991f outposts: don't use custom environment
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 19:12:05 +01:00
e38ee9c580 web: Update Web API Client version (#1944)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-16 16:56:43 +01:00
3bf53b2db1 root: update security
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 16:54:48 +01:00
f33190caa5 release: 2021.12.1 2021-12-16 15:48:59 +01:00
741822424a Merge branch 'master' into version-2021.12 2021-12-16 15:48:53 +01:00
0ca6fbb224 website/docs: final 2021.12.1 release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 15:48:47 +01:00
Sem
f72b652b24 website/integrations: Updated bookstack integration docs page (#1942)
In some cases one might need to define the full SAML property to enable proper group sync. (see: https://github.com/BookStackApp/BookStack/issues/3109 )
2021-12-16 14:23:00 +01:00
0a2c1eb419 web/elements: fix linting error
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 12:27:08 +01:00
eb9593a847 web/elements: close notification drawer when clearing all notifications
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 12:23:44 +01:00
7c71c52791 web/admin: add sidebar to applications
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 12:23:30 +01:00
59493c02c4 web/elements: pass full Markdown object to ak-markdown, get title from metadata
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 12:18:43 +01:00
83089b47d3 web/elements: add Markdown component to improve rendering
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 12:10:46 +01:00
103e723d8c web/elements: add support for sidebar on table page
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 12:10:28 +01:00
7d6e88061f outposts: check if hub from context is set and fallback
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 11:19:57 +01:00
f8aab40e3e internal: cleanup duplicate and redundant code, properly set sentry SDK scope settings
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 11:00:19 +01:00
5123bc1316 root: add sponsors to readme
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 10:30:13 +01:00
30e8408e85 web/admin: fix notification unread colours not matching on user and admin interface
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 10:17:17 +01:00
bb34474101 web/admin: fix stage related flows not being shown in a list
closes #1941

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-16 10:13:10 +01:00
a105760123 events: improve app lookup for event creation
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-15 16:46:02 +01:00
f410a77010 lifecycle: add -Ofair to celery
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-15 16:44:09 +01:00
6ff8fdcc49 root: enable threading integration in sentry
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-15 15:49:08 +01:00
50ca3dc772 core: fix error when attempting to provider from cached application
closes #1940

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-15 15:11:13 +01:00
2a09fc0ae2 release: 2021.12.1-rc5 2021-12-15 10:21:29 +01:00
fbb6756488 Merge branch 'master' into version-2021.12 2021-12-15 10:16:05 +01:00
f45fb2eac0 website/docs: prepare 2021.12.1-rc5
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-15 10:15:58 +01:00
7b8cde17e6 web/admin: show warning when deleting currently logged in user
closes #1937

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-15 10:11:35 +01:00
186634fc67 build(deps): bump @patternfly/patternfly from 4.159.1 to 4.164.2 in /web (#1938) 2021-12-15 08:58:46 +01:00
c84b1b7997 build(deps): bump goauthentik.io/api from 0.2021104.13 to 0.2021104.17 (#1939) 2021-12-15 08:58:30 +01:00
6e83467481 web/flows: fix error when attempting to enroll new webauthn device
closes #1936

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-15 00:24:46 +01:00
72db17f23b stages/identification: fix miscalculated sleep
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 23:31:08 +01:00
ee4e176039 web/admin: fix invalid display for LDAP Source sync status
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 23:00:45 +01:00
e18e681c2b events: dont store full backtrace in systemtask
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 22:55:38 +01:00
10fe67e08d sources/ldap: fix incorrect task names being referenced, use source native slug
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 22:53:14 +01:00
fc1db83be7 web: Update Web API Client version (#1935)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-14 22:19:46 +01:00
3740e65906 web/admin: add dashboard with user creation/login statistics
closes #1867

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 22:08:41 +01:00
30386cd899 events: add custom manager with helpers for metrics
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 21:49:33 +01:00
64a10e9a46 events: fix schema for top_per_user
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 21:08:15 +01:00
77d6242cce web/admin: fix extra closing element
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 20:53:25 +01:00
9a86dcaec3 web: Update Web API Client version (#1934)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-14 16:26:21 +01:00
0b00768b84 events: add flow_execution event type
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 16:13:51 +01:00
d162c79373 flows: fix wrong exception being caught in flow inspector
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 16:06:00 +01:00
05db352a0f web: add link to open API Browser for API Drawer
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 16:03:42 +01:00
5bf3d7fe02 web: Update Web API Client version (#1933)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-14 15:59:26 +01:00
1ae1cbebf4 web/admin: re-organise sidebar items
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 15:57:03 +01:00
8c16dfc478 stages/invitation: use GroupMemberSerializer serializer to prevent all of the user's groups and their users from being returned
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 15:56:13 +01:00
c6a3286e4c web/admin: update overview page
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 15:23:32 +01:00
44cfd7e5b0 web: accept header as slot in PageHeader
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 15:23:20 +01:00
210d4c5058 web: add helper to navigate with params
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 15:23:02 +01:00
6b39d616b1 web/elements: allow aggregate cards' elements to not be centered
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 15:22:52 +01:00
32ace1bece crypto: add additional validation before importing a certificate
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 14:49:25 +01:00
54f893b84f flows: add additional sentry spans
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 11:59:36 +01:00
b5685ec072 outposts: set sentry-trace on API requests to match them to the outer transaction
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 11:50:31 +01:00
5854833240 stages/authenticator_webauthn: fix migrations for different choices
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 11:06:46 +01:00
4b2437a6f1 stages/authenticator_webauthn: use correct choices
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 10:51:34 +01:00
2981ac7b10 tests/e2e: use ghcr for e2e tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 10:36:47 +01:00
59a51c859a stages/authenticator_webauthn: add migration
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 10:09:35 +01:00
47bab6c182 web: Update Web API Client version (#1932)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-14 10:02:50 +01:00
4e6714fffe stages/authenticator_webauthn: make user_verification configurable
closes #1921

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 09:58:20 +01:00
aa6b595545 root: bump python dependencies
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 09:45:36 +01:00
0131b1f6cc sources/oauth: fix wrong redirect URL being generated
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 09:34:47 +01:00
9f53c359dd build(deps): bump typescript from 4.5.3 to 4.5.4 in /web (#1926) 2021-12-14 08:31:56 +01:00
28e4dba3e8 build(deps): bump @babel/core from 7.16.0 to 7.16.5 in /web (#1929) 2021-12-14 08:31:36 +01:00
2afd46e1df build(deps): bump @babel/plugin-transform-runtime in /web (#1922) 2021-12-14 08:31:28 +01:00
f5991b19be build(deps): bump @babel/preset-typescript from 7.16.0 to 7.16.5 in /web (#1925) 2021-12-14 08:31:19 +01:00
5cc75cb25c build(deps): bump @typescript-eslint/parser from 5.6.0 to 5.7.0 in /web (#1923) 2021-12-14 08:31:04 +01:00
68c1df2d39 build(deps): bump @rollup/plugin-node-resolve in /web (#1924) 2021-12-14 08:30:35 +01:00
c83724f45c build(deps): bump @babel/preset-env from 7.16.4 to 7.16.5 in /web (#1930) 2021-12-14 08:29:56 +01:00
5f91c150df build(deps): bump @babel/plugin-proposal-decorators in /web (#1927) 2021-12-14 08:29:36 +01:00
0bfe999442 build(deps): bump @typescript-eslint/eslint-plugin in /web (#1928) 2021-12-14 08:29:13 +01:00
58440b16c4 build(deps): bump goauthentik.io/api from 0.2021104.11 to 0.2021104.13 (#1931) 2021-12-14 08:28:42 +01:00
57757a2ff5 web: Update Web API Client version (#1920) 2021-12-14 01:11:32 +01:00
2993f506a7 sources/oauth: implement apple native sign-in using the apple JS SDK
closes #1881

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 00:40:29 +01:00
e4841d54a1 *: migrate ui_* properties to functions to allow context being passed
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 23:56:35 +01:00
4f05dcec89 sources/oauth: allow oauth types to override their login button challenge
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 23:45:11 +01:00
ede6bcd31e *: remove debug statements from tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 23:41:08 +01:00
728c8e994d sources/oauth: strip parts of custom apple client_id
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 23:26:00 +01:00
5290b64415 web: Update Web API Client version
commit 96533a743c
Author: BeryJu <BeryJu@users.noreply.github.com>
Date:   Mon Dec 13 20:50:45 2021 +0000

    web: Update Web API Client version

    Signed-off-by: GitHub <noreply@github.com>

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 22:53:28 +01:00
fec6de1ba2 providers/oauth2: add additional logging to show with token path is taken
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 22:49:42 +01:00
69678dcfa6 providers/oauth2: use generate_key instead of uuid4
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 22:13:20 +01:00
4911a243ff sources/oauth: add initial okta type
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1910
2021-12-13 21:48:59 +01:00
70316b37da web/admin: only show source name not description
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 21:48:45 +01:00
307cb94e3b website: add initial redirect (#1918)
* website: add initial redirect

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

* website: add integrations too

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

* website: add docs to netlify config

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

* website: use splats correctly

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

* add status

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 20:42:31 +00:00
ace53a8fa5 root: remove lxml version workaround
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 21:08:50 +01:00
0544dc3f83 web: use correct transaction names for web
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 21:03:30 +01:00
708ff300a3 website: remove remaining /index URLs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 19:01:16 +01:00
4e63f0f215 core: add fallback for missing sentry trace
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 18:06:01 +01:00
141481df3a web: send sentry-trace header in API requests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 17:41:11 +01:00
29241cc287 core: always inject sentry trace into template
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 17:41:00 +01:00
e81e97d404 root: add .python-version so dependabot doesn't use broken python 3.10.0
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 17:23:42 +01:00
a5182e5c24 root: custom sentry-sdk, attempt #3
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 17:00:18 +01:00
cf5ff6e160 outposts: reset backoff after successful connect
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:38:48 +01:00
f2b3a2ec91 providers/saml: optimise excessive queries to user when evaluating attributes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:38:38 +01:00
69780c67a9 lib: set evaluation span's description based on filename
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:32:01 +01:00
ac9cf590bc *: use prefixed span names
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:18:42 +01:00
cb6edcb198 core: set tag with request ID
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:15:27 +01:00
8eecc28c3c events: add sentry for geoip
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:15:20 +01:00
10b16bc36a outposts: add description to span
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:12:14 +01:00
2fe88cfea9 root: don't stale enhancement/confirmed
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 15:51:30 +01:00
caab396b56 web/admin: improve wording for froward_auth, don't show setup when using proxy mode
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 15:36:05 +01:00
5f0f4284a2 web/admin: fix rendering for applications on view page
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 15:27:28 +01:00
c11be2284d outposts/proxy: also set max length for redis backend
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 15:05:55 +01:00
aa321196d7 outposts/proxy: fix securecookie: the value is too long again, since it can happen even with filesystem storage
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 13:33:20 +01:00
ff03db61a8 web/admin: fix rendering of applications with custom icon
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 13:21:14 +01:00
f3b3ce6572 website/docs: add 2021.12.1-rc4 release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 12:56:34 +01:00
09b02e1aec release: 2021.12.1-rc4 2021-12-13 12:53:58 +01:00
451a9aaf01 Merge branch 'master' into version-2021.12 2021-12-13 12:53:50 +01:00
eaee7cb562 root: use lxml 4.6.5
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 12:21:27 +01:00
a010c91a52 website/docs: update references for new docusaurus version
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 11:56:26 +01:00
709194330f root: install lxml before regular install to prevent xmlsec issues
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 11:26:05 +01:00
5914bbf173 Merge branch 'master' into version-2021.12
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	Dockerfile
2021-12-13 10:54:21 +01:00
5e9166f859 root: lock lxml to 4.6.4 to prevent xmlsec issues with lxml-version.h missing
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 10:47:59 +01:00
35b8ef6592 build(deps): bump @docusaurus/plugin-client-redirects in /website (#1912)
Bumps [@docusaurus/plugin-client-redirects](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-plugin-client-redirects) from 2.0.0-beta.9 to 2.0.0-beta.13.
- [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.13/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>
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 10:21:33 +01:00
772a939f17 tests/e2e: remove version assertion
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 09:26:07 +01:00
24971801cf build(deps): bump @docusaurus/preset-classic in /website (#1916)
Bumps [@docusaurus/preset-classic](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-preset-classic) from 2.0.0-beta.9 to 2.0.0-beta.13.
- [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.13/packages/docusaurus-preset-classic)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-13 09:24:38 +01:00
43aebe8cb2 build(deps): bump @sentry/tracing from 6.16.0 to 6.16.1 in /web (#1914) 2021-12-13 09:09:32 +01:00
19cfc87c84 build(deps): bump postcss from 8.4.4 to 8.4.5 in /website (#1913) 2021-12-13 08:48:24 +01:00
f920f183c8 build(deps): bump @sentry/browser from 6.16.0 to 6.16.1 in /web (#1915) 2021-12-13 08:47:54 +01:00
97f979c81e build(deps): bump rollup from 2.61.0 to 2.61.1 in /web (#1917) 2021-12-13 08:38:12 +01:00
e61411d396 Revert "Revert "root: use custom sentry-sdk""
This reverts commit c4f985f542.

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 00:01:59 +01:00
c4f985f542 Revert "root: use custom sentry-sdk"
This reverts commit 302dee7ab2.
2021-12-12 23:52:11 +01:00
302dee7ab2 root: use custom sentry-sdk
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 22:11:20 +01:00
83c12ad483 flows: fix description for spans
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 21:51:51 +01:00
4224fd5c6f lib: correctly report "faked" IPs to sentry
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 20:54:29 +01:00
597ce1eb42 Revert "*: use cache.delete_pattern instead of getting keys and deleting them"
This reverts commit ff481ba6e7.

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

# Conflicts:
#	authentik/flows/views/executor.py
#	authentik/policies/signals.py
2021-12-12 20:41:34 +01:00
5ef385f0bb policies: don't always clear application cache on post_save
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 20:39:04 +01:00
cda4be3d47 flows: add additional tags
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 20:37:20 +01:00
8cdf22fc94 root: set default redis iter to 1000
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 20:24:43 +01:00
6efc7578ef flows: add additional sentry spans to flow executor
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 20:04:21 +01:00
4e2457560d outposts/proxy: use filesystem storage for non-embedded outposts
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 17:59:31 +01:00
2ddf122d27 Revert "outposts/proxy: don't save raw jwt in cookie to prevent securecookie: the value is too long"
This reverts commit b3e40c6aed.
2021-12-12 17:58:19 +01:00
a24651437a website/docs: simplify traefik compose example
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 17:18:55 +01:00
30bb7acb17 website/docs: fix escaping for docker-compose annotations
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 17:13:46 +01:00
7859145138 outposts: don't try to create docker client for embedded outpost
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 17:13:26 +01:00
8a8aafec81 root: enable boto3 sentry integration
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 14:38:24 +01:00
deebdf2bcc outposts: fix unlabeled transaction
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 13:46:31 +01:00
4982c4abcb outpost: add additional checks for websocket connection
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-12 00:11:17 +01:00
1486f90077 tests/e2e: cleanup output from e2e containers
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-11 23:27:57 +01:00
f4988bc45e outpost: rewrite re-connect logic without recws
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-11 22:53:59 +01:00
8abc9cc031 outposts: cleanup logs for failed binds
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-11 22:09:18 +01:00
534689895c lib: remove old load_local_files setting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-11 22:03:06 +01:00
8a0dd6be24 outposts: handle RuntimeError during websocket connect
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-11 22:01:55 +01:00
65d2eed82d stagse/authenticator_webauthn: remove pydantic import
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-11 20:32:25 +01:00
e450e7b107 root: add wsproto to default packages
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-11 20:20:28 +01:00
552ddda909 lifecycle: use custom worker class
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-11 19:55:09 +01:00
bafeff7306 outposts: improve logging for outpost controllers
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-11 15:35:20 +01:00
6791436302 root: fix certs file missing in container
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-11 15:01:04 +01:00
7eda794070 outposts: fix docker controller not stopping containers
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1859
2021-12-11 14:00:15 +01:00
e3129c1067 root: bump celery messages to info
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-11 13:59:56 +01:00
ff481ba6e7 *: use cache.delete_pattern instead of getting keys and deleting them
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-10 21:35:28 +01:00
a106bad2db tests/e2e: use correct container image
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-10 21:11:37 +01:00
3a1c311d02 website/docs: Added FortiManager Link (#1908) 2021-12-10 20:57:36 +01:00
6465333f4f website/docs: Add FortiManager intergration instructions (#1907) 2021-12-10 20:57:22 +01:00
b761659227 root: use ghcr for containers during testing
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-10 20:57:09 +01:00
9321c355f8 build(deps): bump construct-style-sheets-polyfill in /web (#1905)
Bumps [construct-style-sheets-polyfill](https://github.com/calebdwilliams/construct-style-sheets) from 2.4.17 to 3.0.5.
- [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-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-10 09:41:24 +01:00
86c8e79ea1 website: rollback to beta.9 to fix build issues
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-10 09:26:05 +01:00
8916b1f8ab build(deps): bump @docusaurus/preset-classic in /website (#1902) 2021-12-10 08:40:01 +01:00
41fcf2aba6 build(deps): bump golang from 1.17.4-bullseye to 1.17.5-bullseye (#1900) 2021-12-10 08:39:36 +01:00
87e72b08a9 build(deps): bump rollup from 2.60.2 to 2.61.0 in /web (#1901) 2021-12-10 08:39:25 +01:00
b2fcd42e3c build(deps): bump typescript from 4.5.2 to 4.5.3 in /web (#1903) 2021-12-10 08:38:30 +01:00
fc1b47a80f build(deps): bump @docusaurus/plugin-client-redirects in /website (#1904) 2021-12-10 08:37:56 +01:00
af14e3502e build(deps): bump goauthentik.io/api from 0.2021104.10 to 0.2021104.11 (#1906) 2021-12-10 08:37:36 +01:00
a2faa5ceb5 tests/e2e: use mixed casing in ldap test to ensure search works
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 20:59:55 +01:00
63a19a1381 outposts/ldap: fix searches with mixed casing
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 20:55:51 +01:00
b472dcb7e7 tests/e2e: update new outpost service account names
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 20:44:52 +01:00
6303909031 web: fix linting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 20:16:53 +01:00
4bdc06865b web: fix borders of sidebars in dark mode
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 20:10:14 +01:00
2ee48cd039 outposts: set display name for outpost service account
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 19:59:38 +01:00
893d5f452b web: Update Web API Client version (#1899)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-09 19:56:29 +01:00
340a9bc8ee core: fix error when using invalid key-values in attributes query
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 19:53:47 +01:00
cb3d9f83f1 ci: don't rebuild frontend for sentry, extract files from container image
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 14:10:13 +01:00
4ba55aa8e9 flows: fix error when trying to print FlowToken objects
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 13:55:43 +01:00
bab6f501ec flows: fix error in inspector view
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 13:52:46 +01:00
7327939684 website/docs: add 2021.12.1-rc3
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 13:25:23 +01:00
ffb0135f06 release: 2021.12.1-rc3 2021-12-09 13:23:41 +01:00
ee0ddc3d17 Merge branch 'master' into version-2021.12 2021-12-09 13:23:28 +01:00
5dd979d66c root: add flower entrypoint
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 11:38:57 +01:00
a9bd34f3c5 events: revert to @prefill_task decorator since base class doesn't get executed until task runs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 10:18:00 +01:00
db316b59c5 stages/prompt: use policyenginemode all
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 09:39:40 +01:00
6209714f87 policies/expression: add ak_call_policy
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 09:39:28 +01:00
1ed2bddba7 root: fix celery task ID not being included in log
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-09 09:36:52 +01:00
26b35c9b7b root: fix name conflict in threadlocal
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-08 21:42:48 +01:00
86a9271f75 root: add request_id to celery tasks, prefixed with "task-"
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-08 21:34:20 +01:00
402ed9bd20 root: allow usage of --randomly-seed for testing
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-08 21:33:41 +01:00
68a0684569 ci: fix test-migrations-from-stable (#1898)
* ci: copy files instead of checking out

* ci: add marks for prs
2021-12-08 21:00:58 +01:00
bd2e453218 outposts/ldap: Fix search case sensitivity. (#1897) 2021-12-08 20:11:56 +01:00
1f31c63e57 build(deps): bump python from 3.9-slim-bullseye to 3.10.1-slim-bullseye (#1894) 2021-12-08 07:49:01 +01:00
480410efa2 build(deps): bump @sentry/tracing from 6.15.0 to 6.16.0 in /web (#1895) 2021-12-08 07:48:33 +01:00
e9bfee52ed build(deps): bump @sentry/browser from 6.15.0 to 6.16.0 in /web (#1896) 2021-12-08 07:47:03 +01:00
326b574d54 root: update dependencies
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-07 16:25:10 +01:00
0a7abcf2ad build(deps): bump @typescript-eslint/eslint-plugin in /web (#1890) 2021-12-07 08:41:43 +01:00
9e5019881e build(deps): bump @typescript-eslint/parser from 5.5.0 to 5.6.0 in /web (#1892) 2021-12-07 08:40:49 +01:00
8071750681 build(deps): bump eslint from 8.4.0 to 8.4.1 in /web (#1891) 2021-12-07 08:39:49 +01:00
f2f0931904 build(deps): bump goauthentik.io/api from 0.2021104.9 to 0.2021104.10 (#1893) 2021-12-07 08:39:10 +01:00
a91204e5b9 web/user: allow custom font-awesome icons for applications
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1189
2021-12-06 21:20:15 +01:00
b14c22cbff web: fix duplicate classes, make generic icon clickable
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-06 21:13:04 +01:00
b3e40c6aed outposts/proxy: don't save raw jwt in cookie to prevent securecookie: the value is too long
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-06 13:54:59 +01:00
873aa4bb22 providers/saml: remove SESSION_KEY_POST from session after using it
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1873
2021-12-06 12:47:25 +01:00
c1ea78c422 core: fix missing permission check for group creating when creating service account
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-06 12:33:29 +01:00
3c8bbc2621 sources/*: only allow superusers to directly create source connections
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-06 12:22:40 +01:00
42a9979d91 web/elements: close dropdown when refresh event is dispatched
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-06 11:18:22 +01:00
b7f94df4d9 web: fix text colour for bad request on light mode
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-06 10:54:21 +01:00
4143d3fe28 events: don't set metrics on import
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-06 10:13:48 +01:00
f95c06b76f web: Update Web API Client version (#1889)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-06 10:13:42 +01:00
e3e9178ccc web/admin: show outpost warning on application page too
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-06 10:10:44 +01:00
b694816e7b sources/*: Allow creation of source connections via API
closes #1888

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-06 10:05:42 +01:00
e046000f36 build(deps): bump chart.js from 3.6.1 to 3.6.2 in /web (#1886) 2021-12-06 08:42:21 +01:00
edb5caae9b build(deps-dev): bump prettier from 2.5.0 to 2.5.1 in /website (#1883) 2021-12-06 08:41:30 +01:00
02d27651f3 build(deps): bump eslint from 8.3.0 to 8.4.0 in /web (#1884) 2021-12-06 08:41:05 +01:00
44cd4d847d build(deps): bump golang from 1.17.3-bullseye to 1.17.4-bullseye (#1882) 2021-12-06 08:40:18 +01:00
472256794d build(deps): bump prettier from 2.5.0 to 2.5.1 in /web (#1885) 2021-12-06 08:39:52 +01:00
cbb6887983 build(deps): bump goauthentik.io/api from 0.2021104.7 to 0.2021104.9 (#1887) 2021-12-06 08:39:26 +01:00
317e9ec605 core: add FlowToken which saves the pickled flow plan, replace standard token in email stage to allow finishing flows in different sessions
closes #1801

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-05 15:20:11 +01:00
ada2a16412 tests/e2e: add post binding test
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-05 11:18:01 +01:00
61f6b0f122 web: Update Web API Client version (#1880)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-05 11:17:31 +01:00
6a3f7e45cf providers/saml: add ?force_binding to limit bindings for metadata endpoint
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-05 11:14:42 +01:00
2b78c4ba86 *: use request.query_params instead of accessing the django request
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-05 11:14:20 +01:00
680ef641fb providers/saml: fix error when propertymapping returns invalid data in list
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-05 10:31:16 +01:00
6c23fc4b2b webiste/docs: add 2021.12.1-rc2 release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-04 20:03:26 +01:00
322 changed files with 10182 additions and 7713 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2021.12.1-rc2
current_version = 2021.12.4
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
@ -17,8 +17,6 @@ values =
beta
stable
[bumpversion:file:website/docs/installation/docker-compose.md]
[bumpversion:file:docker-compose.yml]
[bumpversion:file:schema.yml]
@ -30,7 +28,3 @@ values =
[bumpversion:file:internal/constants/constants.go]
[bumpversion:file:web/src/constants.ts]
[bumpversion:file:website/docs/outposts/manual-deploy-docker-compose.md]
[bumpversion:file:website/docs/outposts/manual-deploy-kubernetes.md]

1
.github/stale.yml vendored
View File

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

View File

@ -47,7 +47,7 @@ jobs:
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: scripts/ci_prepare.sh
- name: run pylint
- name: run job
run: pipenv run make ci-${{ matrix.job }}
test-migrations:
runs-on: ubuntu-latest
@ -86,12 +86,18 @@ jobs:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
- name: checkout stable
id: stable
run: |
# Save current branch
current=$(git branch --show)
echo ##[set-output name=originalBranch]$current
# Copy current, latest config to local
cp authentik/lib/default.yml local.env.yml
cp -R .github ..
cp -R scripts ..
git checkout $(git describe --abbrev=0 --match 'version/*')
git checkout $GITHUB_HEAD_REF -- .github
git checkout $GITHUB_HEAD_REF -- scripts
rm -rf .github/ scripts/
mv ../.github ../scripts .
- name: prepare
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
@ -105,7 +111,8 @@ jobs:
run: |
set -x
git fetch
git checkout $GITHUB_HEAD_REF
git reset --hard HEAD
git checkout ${{ steps.stable.outputs.originalBranch }}
pipenv sync --dev
- name: prepare
env:
@ -173,7 +180,7 @@ jobs:
testspace [integration]unittest.xml --link=codecov
- if: ${{ always() }}
uses: codecov/codecov-action@v2
test-e2e:
test-e2e-provider:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -212,22 +219,75 @@ jobs:
npm run build
- name: run e2e
run: |
pipenv run make test-e2e
pipenv run make test-e2e-provider
pipenv run coverage xml
- name: run testspace
if: ${{ always() }}
run: |
testspace [e2e]unittest.xml --link=codecov
testspace [e2e-provider]unittest.xml --link=codecov
- if: ${{ always() }}
uses: codecov/codecov-action@v2
build:
test-e2e-rest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- uses: testspace-com/setup-testspace@v1
with:
domain: ${{github.repository_owner}}
- id: cache-pipenv
uses: actions/cache@v2.1.7
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
- name: prepare
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
run: |
scripts/ci_prepare.sh
docker-compose -f tests/e2e/docker-compose.yml up -d
- id: cache-web
uses: actions/cache@v2.1.7
with:
path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/**') }}
- name: prepare web ui
if: steps.cache-web.outputs.cache-hit != 'true'
run: |
cd web
npm i
npm run build
- name: run e2e
run: |
pipenv run make test-e2e-rest
pipenv run coverage xml
- name: run testspace
if: ${{ always() }}
run: |
testspace [e2e-rest]unittest.xml --link=codecov
- if: ${{ always() }}
uses: codecov/codecov-action@v2
ci-core-mark:
needs:
- lint
- test-migrations
- test-migrations-from-stable
- test-unittest
- test-integration
- test-e2e
- test-e2e-rest
- test-e2e-provider
runs-on: ubuntu-latest
steps:
- run: echo mark
build:
needs: ci-core-mark
runs-on: ubuntu-latest
timeout-minutes: 120
strategy:
@ -244,7 +304,7 @@ jobs:
- name: prepare variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
python ./scripts/gh_env.py
- name: Login to Container Registry

View File

@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '^1.16.3'
go-version: "^1.17"
- name: Run linter
run: |
# Create folder structure for go embeds
@ -30,10 +30,16 @@ jobs:
-w /app \
golangci/golangci-lint:v1.39.0 \
golangci-lint run -v --timeout 200s
ci-outpost-mark:
needs:
- lint-golint
runs-on: ubuntu-latest
steps:
- run: echo mark
build:
timeout-minutes: 120
needs:
- lint-golint
- ci-outpost-mark
strategy:
fail-fast: false
matrix:
@ -52,7 +58,7 @@ jobs:
- name: prepare variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
python ./scripts/gh_env.py
- name: Login to Container Registry
@ -74,3 +80,41 @@ jobs:
build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
platforms: ${{ matrix.arch }}
build-outpost-binary:
timeout-minutes: 120
needs:
- ci-outpost-mark
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
type:
- proxy
- ldap
goos: [linux]
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: "^1.17"
- uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- name: Build web
run: |
cd web
npm install
npm run build-proxy
- name: Build outpost
run: |
set -x
export GOOS=${{ matrix.goos }}
export GOARCH=${{ matrix.goarch }}
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
- uses: actions/upload-artifact@v2
with:
name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
path: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}

View File

@ -65,12 +65,18 @@ jobs:
run: |
cd web
npm run lit-analyse
build:
ci-web-mark:
needs:
- lint-eslint
- lint-prettier
- lint-lit-analyse
runs-on: ubuntu-latest
steps:
- run: echo mark
build:
needs:
- ci-web-mark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2

View File

@ -30,14 +30,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik:2021.12.1-rc2,
beryju/authentik:2021.12.4,
beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.12.1-rc2,
ghcr.io/goauthentik/server:2021.12.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.1-rc2', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2021.12.4', 'rc') }}
run: |
docker pull beryju/authentik:latest
docker tag beryju/authentik:latest beryju/authentik:stable
@ -57,7 +57,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: "^1.15"
go-version: "^1.17"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
- name: Set up Docker Buildx
@ -78,14 +78,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-${{ matrix.type }}:2021.12.1-rc2,
beryju/authentik-${{ matrix.type }}:2021.12.4,
beryju/authentik-${{ matrix.type }}:latest,
ghcr.io/goauthentik/${{ matrix.type }}:2021.12.1-rc2,
ghcr.io/goauthentik/${{ matrix.type }}:2021.12.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.1-rc2', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2021.12.4', 'rc') }}
run: |
docker pull beryju/authentik-${{ matrix.type }}:latest
docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable
@ -93,10 +93,50 @@ jobs:
docker pull ghcr.io/goauthentik/${{ matrix.type }}:latest
docker tag ghcr.io/goauthentik/${{ matrix.type }}:latest ghcr.io/goauthentik/${{ matrix.type }}:stable
docker push ghcr.io/goauthentik/${{ matrix.type }}:stable
build-outpost-binary:
timeout-minutes: 120
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
type:
- proxy
- ldap
goos: [linux, darwin]
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: "^1.17"
- uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- name: Build web
run: |
cd web
npm install
npm run build-proxy
- name: Build outpost
run: |
set -x
export GOOS=${{ matrix.goos }}
export GOARCH=${{ matrix.goarch }}
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
- name: Upload binaries to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
asset_name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
tag: ${{ github.ref }}
test-release:
needs:
- build-server
- build-outpost
- build-outpost-binary
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -110,20 +150,17 @@ jobs:
docker-compose run -u root server test
sentry-release:
needs:
- test-release
- build-server
- build-outpost
- build-outpost-binary
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Build web api client and web ui
- name: Get static files from docker image
run: |
export NODE_ENV=production
cd web
npm i
npm run build
docker pull ghcr.io/goauthentik/server:latest
container=$(docker container create ghcr.io/goauthentik/server:latest)
docker cp ${container}:web/ .
- name: Create a Sentry.io release
uses: getsentry/action-release@v1
if: ${{ github.event_name == 'release' }}
@ -133,7 +170,7 @@ jobs:
SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org
with:
version: authentik@2021.12.1-rc2
version: authentik@2021.12.4
environment: beryjuorg-prod
sourcemaps: './web/dist'
url_prefix: '~/static/dist'

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.9.7

View File

@ -11,7 +11,8 @@
"saml",
"totp",
"webauthn",
"traefik"
"traefik",
"passwordless"
],
"python.linting.pylintEnabled": true,
"todo-tree.tree.showCountsInTree": true,

View File

@ -1,5 +1,5 @@
# Stage 1: Lock python dependencies
FROM docker.io/python:3.9-slim-bullseye as locker
FROM docker.io/python:3.10.1-slim-bullseye as locker
COPY ./Pipfile /app/
COPY ./Pipfile.lock /app/
@ -28,7 +28,7 @@ ENV NODE_ENV=production
RUN cd /work/web && npm i && npm run build
# Stage 4: Build go proxy
FROM docker.io/golang:1.17.3-bullseye AS builder
FROM docker.io/golang:1.17.5-bullseye AS builder
WORKDIR /work
@ -44,7 +44,7 @@ COPY ./go.sum /work/go.sum
RUN go build -o /work/authentik ./cmd/server/main.go
# Stage 5: Run
FROM docker.io/python:3.9-slim-bullseye
FROM docker.io/python:3.10.1-slim-bullseye
WORKDIR /
COPY --from=locker /app/requirements.txt /
@ -64,8 +64,8 @@ RUN apt-get update && \
apt-get clean && \
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
mkdir /backups /certs && \
chown authentik:authentik /backups /certs
mkdir -p /backups /certs /media && \
chown authentik:authentik /backups /certs /media
COPY ./authentik/ /authentik
COPY ./pyproject.toml /

View File

@ -4,13 +4,16 @@ UID = $(shell id -u)
GID = $(shell id -g)
NPM_VERSION = $(shell python -m scripts.npm_version)
all: lint-fix lint test gen
all: lint-fix lint test gen web
test-integration:
coverage run manage.py test tests/integration
test-e2e:
coverage run manage.py test tests/e2e
test-e2e-provider:
coverage run manage.py test tests/e2e/test_provider*
test-e2e-rest:
coverage run manage.py test tests/e2e/test_flows* tests/e2e/test_source*
test:
coverage run manage.py test authentik
@ -32,6 +35,7 @@ lint-fix:
lint:
bandit -r authentik tests lifecycle -x node_modules
pylint authentik tests lifecycle
golangci-lint run -v
i18n-extract: i18n-extract-core web-extract
@ -84,6 +88,9 @@ migrate:
run:
go run -v cmd/server/main.go
web-watch:
cd web && npm run watch
web: web-lint-fix web-lint web-extract
web-lint-fix:

View File

@ -39,7 +39,7 @@ pycryptodome = "*"
pyjwt = "*"
pyyaml = "*"
requests-oauthlib = "*"
sentry-sdk = "*"
sentry-sdk = { git = 'https://github.com/beryju/sentry-python.git', ref = '379aee28b15d3b87b381317746c4efd24b3d7bc3' }
service_identity = "*"
structlog = "*"
swagger-spec-validator = "*"
@ -49,6 +49,8 @@ urllib3 = {extras = ["secure"],version = "*"}
uvicorn = {extras = ["standard"],version = "*"}
webauthn = "*"
xmlsec = "*"
flower = "*"
wsproto = "*"
[dev-packages]
bandit = "*"

865
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -38,3 +38,23 @@ See [Development Documentation](https://goauthentik.io/developer-docs/?utm_sourc
## Security
See [SECURITY.md](SECURITY.md)
## Sponsors
This project is proudly sponsored by:
<p>
<a href="https://www.digitalocean.com/?utm_medium=opensource&utm_source=goauthentik.io">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
</a>
</p>
DigitalOcean provides development and testing resources for authentik.
<p>
<a href="https://www.netlify.com">
<img src="https://www.netlify.com/img/global/badges/netlify-color-accent.svg" alt="Deploys by Netlify" />
</a>
</p>
Netlify hosts the [goauthentik.io](goauthentik.io) site.

View File

@ -6,8 +6,8 @@
| Version | Supported |
| ---------- | ------------------ |
| 2021.9.x | :white_check_mark: |
| 2021.10.x | :white_check_mark: |
| 2021.12.x | :white_check_mark: |
## Reporting a Vulnerability

View File

@ -1,3 +1,3 @@
"""authentik"""
__version__ = "2021.12.1-rc2"
__version__ = "2021.12.4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -1,13 +1,6 @@
"""authentik administration metrics"""
import time
from collections import Counter
from datetime import timedelta
from django.db.models import Count, ExpressionWrapper, F
from django.db.models.fields import DurationField
from django.db.models.functions import ExtractHour
from django.utils.timezone import now
from drf_spectacular.utils import extend_schema, extend_schema_field
from guardian.shortcuts import get_objects_for_user
from rest_framework.fields import IntegerField, SerializerMethodField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
@ -15,31 +8,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import Event, EventAction
def get_events_per_1h(**filter_kwargs) -> list[dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
date_from = now() - timedelta(days=1)
result = (
Event.objects.filter(created__gte=date_from, **filter_kwargs)
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
.annotate(age_hours=ExtractHour("age"))
.values("age_hours")
.annotate(count=Count("pk"))
.order_by("age_hours")
)
data = Counter({int(d["age_hours"]): d["count"] for d in result})
results = []
_now = now()
for hour in range(0, -24, -1):
results.append(
{
"x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
"y_cord": data[hour * -1],
}
)
return results
from authentik.events.models import EventAction
class CoordinateSerializer(PassiveSerializer):
@ -58,12 +27,22 @@ class LoginMetricsSerializer(PassiveSerializer):
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_per_1h(self, _):
"""Get successful logins per hour for the last 24 hours"""
return get_events_per_1h(action=EventAction.LOGIN)
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN)
.get_events_per_hour()
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_failed_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
return get_events_per_1h(action=EventAction.LOGIN_FAILED)
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN_FAILED)
.get_events_per_hour()
)
class AdministrationMetricsViewSet(APIView):
@ -75,4 +54,5 @@ class AdministrationMetricsViewSet(APIView):
def get(self, request: Request) -> Response:
"""Login Metrics per 1h"""
serializer = LoginMetricsSerializer(True)
serializer.context["user"] = request.user
return Response(serializer.data)

View File

@ -11,7 +11,12 @@ from structlog.stdlib import get_logger
from authentik import ENV_GIT_HASH_KEY, __version__
from authentik.events.models import Event, EventAction, Notification
from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
from authentik.events.monitored_tasks import (
MonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
from authentik.lib.config import CONFIG
from authentik.lib.utils.http import get_http_session
from authentik.root.celery import CELERY_APP
@ -48,8 +53,9 @@ def clear_update_notifications():
notification.delete()
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
def update_latest_version(self: PrefilledMonitoredTask):
@CELERY_APP.task(bind=True, base=MonitoredTask)
@prefill_task
def update_latest_version(self: MonitoredTask):
"""Update latest version info"""
if CONFIG.y_bool("disable_update_check"):
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)

View File

@ -5,6 +5,7 @@ from django.http.response import HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField
from rest_framework.parsers import MultiPartParser
@ -15,7 +16,7 @@ from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
@ -239,8 +240,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Metrics for application logins"""
app = self.get_object()
return Response(
get_events_per_1h(
get_objects_for_user(request.user, "authentik_events.view_event")
.filter(
action=EventAction.AUTHORIZE_APPLICATION,
context__authorized_application__pk=app.pk.hex,
)
.get_events_per_hour()
)

View File

@ -1,9 +1,11 @@
"""Groups API Viewset"""
from json import loads
from django.db.models.query import QuerySet
from django_filters.filters import ModelMultipleChoiceFilter
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
from django_filters.filterset import FilterSet
from rest_framework.fields import CharField, JSONField
from rest_framework.serializers import ListSerializer, ModelSerializer
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
@ -62,6 +64,13 @@ class GroupSerializer(ModelSerializer):
class GroupFilter(FilterSet):
"""Filter for groups"""
attributes = CharFilter(
field_name="attributes",
lookup_expr="",
label="Attributes",
method="filter_attributes",
)
members_by_username = ModelMultipleChoiceFilter(
field_name="users__username",
to_field_name="username",
@ -72,10 +81,28 @@ class GroupFilter(FilterSet):
queryset=User.objects.all(),
)
# pylint: disable=unused-argument
def filter_attributes(self, queryset, name, value):
"""Filter attributes by query args"""
try:
value = loads(value)
except ValueError:
raise ValidationError(detail="filter: failed to parse JSON")
if not isinstance(value, dict):
raise ValidationError(detail="filter: value must be key:value mapping")
qs = {}
for key, _value in value.items():
qs[f"attributes__{key}"] = _value
try:
_ = len(queryset.filter(**qs))
return queryset.filter(**qs)
except ValueError:
return queryset
class Meta:
model = Group
fields = ["name", "is_superuser", "members_by_pk", "members_by_username"]
fields = ["name", "is_superuser", "members_by_pk", "attributes", "members_by_username"]
class GroupViewSet(UsedByMixin, ModelViewSet):

View File

@ -104,14 +104,14 @@ class SourceViewSet(
)
matching_sources: list[UserSettingSerializer] = []
for source in _all_sources:
user_settings = source.ui_user_settings
user_settings = source.ui_user_settings()
if not user_settings:
continue
policy_engine = PolicyEngine(source, request.user, request)
policy_engine.build()
if not policy_engine.passing:
continue
source_settings = source.ui_user_settings
source_settings = source.ui_user_settings()
source_settings.initial_data["object_uid"] = source.slug
if not source_settings.is_valid():
LOGGER.warning(source_settings.errors)

View File

@ -38,7 +38,7 @@ from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
@ -184,19 +184,31 @@ class UserMetricsSerializer(PassiveSerializer):
def get_logins_per_1h(self, _):
"""Get successful logins per hour for the last 24 hours"""
user = self.context["user"]
return get_events_per_1h(action=EventAction.LOGIN, user__pk=user.pk)
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN, user__pk=user.pk)
.get_events_per_hour()
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_failed_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
user = self.context["user"]
return get_events_per_1h(action=EventAction.LOGIN_FAILED, context__username=user.username)
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN_FAILED, context__username=user.username)
.get_events_per_hour()
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_authorizations_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
user = self.context["user"]
return get_events_per_1h(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
.get_events_per_hour()
)
class UsersFilter(FilterSet):
@ -233,7 +245,11 @@ class UsersFilter(FilterSet):
qs = {}
for key, _value in value.items():
qs[f"attributes__{key}"] = _value
return queryset.filter(**qs)
try:
_ = len(queryset.filter(**qs))
return queryset.filter(**qs)
except ValueError:
return queryset
class Meta:
model = User
@ -314,7 +330,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
name=username,
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
)
if create_group:
if create_group and self.request.user.has_perm("authentik_core.add_group"):
group = Group.objects.create(
name=username,
)

View File

@ -5,6 +5,7 @@ from typing import Callable
from uuid import uuid4
from django.http import HttpRequest, HttpResponse
from sentry_sdk.api import set_tag
SESSION_IMPERSONATE_USER = "authentik_impersonate_user"
SESSION_IMPERSONATE_ORIGINAL_USER = "authentik_impersonate_original_user"
@ -50,6 +51,7 @@ class RequestIDMiddleware:
"request_id": request_id,
"host": request.get_host(),
}
set_tag("authentik.request_id", request_id)
response = self.get_response(request)
response[RESPONSE_HEADER_ID] = request.request_id
setattr(response, "ak_context", {})
@ -65,4 +67,6 @@ def structlog_add_request_id(logger: Logger, method_name: str, event_dict: dict)
"""If threadlocal has authentik defined, add request_id to log"""
if hasattr(LOCAL, "authentik"):
event_dict.update(LOCAL.authentik)
if hasattr(LOCAL, "authentik_task"):
event_dict.update(LOCAL.authentik_task)
return event_dict

View File

@ -25,7 +25,6 @@ from structlog.stdlib import get_logger
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.signals import password_changed
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.flows.models import Flow
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
from authentik.lib.models import CreatedUpdatedModel, DomainlessURLValidator, SerializerModel
@ -203,7 +202,7 @@ class Provider(SerializerModel):
name = models.TextField()
authorization_flow = models.ForeignKey(
Flow,
"authentik_flows.Flow",
on_delete=models.CASCADE,
help_text=_("Flow used when authorizing this provider."),
related_name="provider_authorization",
@ -263,7 +262,7 @@ class Application(PolicyBindingModel):
it is returned as-is"""
if not self.meta_icon:
return None
if self.meta_icon.name.startswith("http") or self.meta_icon.name.startswith("/static"):
if "://" in self.meta_icon.name or self.meta_icon.name.startswith("/static"):
return self.meta_icon.name
return self.meta_icon.url
@ -271,15 +270,21 @@ class Application(PolicyBindingModel):
"""Get launch URL if set, otherwise attempt to get launch URL based on provider."""
if self.meta_launch_url:
return self.meta_launch_url
if self.provider:
return self.get_provider().launch_url
if provider := self.get_provider():
return provider.launch_url
return None
def get_provider(self) -> Optional[Provider]:
"""Get casted provider instance"""
if not self.provider:
return None
return Provider.objects.get_subclass(pk=self.provider.pk)
# if the Application class has been cache, self.provider is set
# but doing a direct query lookup will fail.
# In that case, just return None
try:
return Provider.objects.get_subclass(pk=self.provider.pk)
except Provider.DoesNotExist:
return None
def __str__(self):
return self.name
@ -324,7 +329,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True)
authentication_flow = models.ForeignKey(
Flow,
"authentik_flows.Flow",
blank=True,
null=True,
default=None,
@ -333,7 +338,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
related_name="source_authentication",
)
enrollment_flow = models.ForeignKey(
Flow,
"authentik_flows.Flow",
blank=True,
null=True,
default=None,
@ -360,13 +365,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
"""Return component used to edit this object"""
raise NotImplementedError
@property
def ui_login_button(self) -> Optional[UILoginButton]:
def ui_login_button(self, request: HttpRequest) -> Optional[UILoginButton]:
"""If source uses a http-based flow, return UI Information about the login
button. If source doesn't use http-based flow, return None."""
return None
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
"""Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or UserSettingSerializer."""
@ -453,6 +456,14 @@ class Token(ManagedModel, ExpiringModel):
"""Handler which is called when this object is expired."""
from authentik.events.models import Event, EventAction
if self.intent in [
TokenIntents.INTENT_RECOVERY,
TokenIntents.INTENT_VERIFICATION,
TokenIntents.INTENT_APP_PASSWORD,
]:
super().expire_action(*args, **kwargs)
return
self.key = default_token_key()
self.expires = default_token_duration()
self.save(*args, **kwargs)

View File

@ -16,15 +16,21 @@ from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
from structlog.stdlib import get_logger
from authentik.core.models import AuthenticatedSession, ExpiringModel
from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
from authentik.events.monitored_tasks import (
MonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
from authentik.lib.config import CONFIG
from authentik.root.celery import CELERY_APP
LOGGER = get_logger()
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
def clean_expired_models(self: PrefilledMonitoredTask):
@CELERY_APP.task(bind=True, base=MonitoredTask)
@prefill_task
def clean_expired_models(self: MonitoredTask):
"""Remove expired objects"""
messages = []
for cls in ExpiringModel.__subclasses__():
@ -62,8 +68,9 @@ def should_backup() -> bool:
return True
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
def backup_database(self: PrefilledMonitoredTask): # pragma: no cover
@CELERY_APP.task(bind=True, base=MonitoredTask)
@prefill_task
def backup_database(self: MonitoredTask): # pragma: no cover
"""Database backup"""
self.result_timeout_hours = 25
if not should_backup():

View File

@ -19,6 +19,7 @@
<script src="{% static 'dist/poly.js' %}" type="module"></script>
{% block head %}
{% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" />
</head>
<body>
{% block body %}

View File

@ -2,7 +2,7 @@
from time import sleep
from typing import Callable, Type
from django.test import TestCase
from django.test import RequestFactory, TestCase
from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user
@ -30,6 +30,9 @@ class TestModels(TestCase):
def source_tester_factory(test_model: Type[Stage]) -> Callable:
"""Test source"""
factory = RequestFactory()
request = factory.get("/")
def tester(self: TestModels):
model_class = None
if test_model._meta.abstract:
@ -38,8 +41,8 @@ def source_tester_factory(test_model: Type[Stage]) -> Callable:
model_class = test_model()
model_class.slug = "test"
self.assertIsNotNone(model_class.component)
_ = model_class.ui_login_button
_ = model_class.ui_user_settings
_ = model_class.ui_login_button(request)
_ = model_class.ui_user_settings()
return tester

View File

@ -41,7 +41,7 @@ class TestPropertyMappingAPI(APITestCase):
expr = "return True"
self.assertEqual(PropertyMappingSerializer().validate_expression(expr), expr)
with self.assertRaises(ValidationError):
print(PropertyMappingSerializer().validate_expression("/"))
PropertyMappingSerializer().validate_expression("/")
def test_types(self):
"""Test PropertyMappigns's types endpoint"""

View File

@ -54,7 +54,9 @@ class TestTokenAPI(APITestCase):
def test_token_expire(self):
"""Test Token expire task"""
token: Token = Token.objects.create(expires=now(), user=get_anonymous_user())
token: Token = Token.objects.create(
expires=now(), user=get_anonymous_user(), intent=TokenIntents.INTENT_API
)
key = token.key
clean_expired_models.delay().get()
token.refresh_from_db()

View File

@ -72,7 +72,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
return value
def validate_key_data(self, value: str) -> str:
"""Verify that input is a valid PEM RSA Key"""
"""Verify that input is a valid PEM Key"""
# Since this field is optional, data can be empty.
if value != "":
try:
@ -192,7 +192,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
secret=certificate,
type="certificate",
).from_http(request)
if "download" in request._request.GET:
if "download" in request.query_params:
# Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html
response = HttpResponse(
certificate.certificate_data, content_type="application/x-pem-file"
@ -223,7 +223,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
secret=certificate,
type="private_key",
).from_http(request)
if "download" in request._request.GET:
if "download" in request.query_params:
# Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html
response = HttpResponse(certificate.key_data, content_type="application/x-pem-file")
response[

View File

@ -44,7 +44,7 @@ class CertificateBuilder:
"""Build self-signed certificate"""
one_day = datetime.timedelta(1, 0, 0)
self.__private_key = rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=default_backend()
public_exponent=65537, key_size=4096, backend=default_backend()
)
self.__public_key = self.__private_key.public_key()
alt_names: list[x509.GeneralName] = [x509.DNSName(x) for x in subject_alt_names or []]

View File

@ -1,20 +1,28 @@
"""authentik crypto models"""
from binascii import hexlify
from hashlib import md5
from typing import Optional
from typing import Optional, Union
from uuid import uuid4
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric.ec import (
EllipticCurvePrivateKey,
EllipticCurvePublicKey,
)
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import Certificate, load_pem_x509_certificate
from django.db import models
from django.utils.translation import gettext_lazy as _
from structlog.stdlib import get_logger
from authentik.lib.models import CreatedUpdatedModel
from authentik.managed.models import ManagedModel
LOGGER = get_logger()
class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
"""CertificateKeyPair that can be used for signing or encrypting if `key_data`
@ -33,8 +41,8 @@ class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
)
_cert: Optional[Certificate] = None
_private_key: Optional[RSAPrivateKey] = None
_public_key: Optional[RSAPublicKey] = None
_private_key: Optional[Union[RSAPrivateKey, EllipticCurvePrivateKey, Ed25519PrivateKey]] = None
_public_key: Optional[Union[RSAPublicKey, EllipticCurvePublicKey, Ed25519PublicKey]] = None
@property
def certificate(self) -> Certificate:
@ -46,14 +54,16 @@ class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
return self._cert
@property
def public_key(self) -> Optional[RSAPublicKey]:
def public_key(self) -> Optional[Union[RSAPublicKey, EllipticCurvePublicKey, Ed25519PublicKey]]:
"""Get public key of the private key"""
if not self._public_key:
self._public_key = self.private_key.public_key()
return self._public_key
@property
def private_key(self) -> Optional[RSAPrivateKey]:
def private_key(
self,
) -> Optional[Union[RSAPrivateKey, EllipticCurvePrivateKey, Ed25519PrivateKey]]:
"""Get python cryptography PrivateKey instance"""
if not self._private_key and self.key_data != "":
try:
@ -62,7 +72,8 @@ class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
password=None,
backend=default_backend(),
)
except ValueError:
except ValueError as exc:
LOGGER.warning(exc)
return None
return self._private_key

View File

@ -2,11 +2,19 @@
from glob import glob
from pathlib import Path
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509.base import load_pem_x509_certificate
from django.utils.translation import gettext_lazy as _
from structlog.stdlib import get_logger
from authentik.crypto.models import CertificateKeyPair
from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
from authentik.events.monitored_tasks import (
MonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
from authentik.lib.config import CONFIG
from authentik.root.celery import CELERY_APP
@ -15,9 +23,26 @@ LOGGER = get_logger()
MANAGED_DISCOVERED = "goauthentik.io/crypto/discovered/%s"
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
def certificate_discovery(self: PrefilledMonitoredTask):
"""Discover and update certificates form the filesystem"""
def ensure_private_key_valid(body: str):
"""Attempt loading of a PEM Private key without password"""
load_pem_private_key(
str.encode("\n".join([x.strip() for x in body.split("\n")])),
password=None,
backend=default_backend(),
)
return body
def ensure_certificate_valid(body: str):
"""Attempt loading of a PEM-encoded certificate"""
load_pem_x509_certificate(body.encode("utf-8"), default_backend())
return body
@CELERY_APP.task(bind=True, base=MonitoredTask)
@prefill_task
def certificate_discovery(self: MonitoredTask):
"""Discover, import and update certificates from the filesystem"""
certs = {}
private_keys = {}
discovered = 0
@ -27,6 +52,9 @@ def certificate_discovery(self: PrefilledMonitoredTask):
continue
if path.is_dir():
continue
# For certbot setups, we want to ignore archive.
if "archive" in file:
continue
# Support certbot's directory structure
if path.name in ["fullchain.pem", "privkey.pem"]:
cert_name = path.parent.name
@ -35,12 +63,12 @@ def certificate_discovery(self: PrefilledMonitoredTask):
try:
with open(path, "r+", encoding="utf-8") as _file:
body = _file.read()
if "BEGIN RSA PRIVATE KEY" in body:
private_keys[cert_name] = body
if "PRIVATE KEY" in body:
private_keys[cert_name] = ensure_private_key_valid(body)
else:
certs[cert_name] = body
except OSError as exc:
LOGGER.warning("Failed to open file", exc=exc, file=path)
certs[cert_name] = ensure_certificate_valid(body)
except (OSError, ValueError) as exc:
LOGGER.warning("Failed to open file or invalid format", exc=exc, file=path)
discovered += 1
for name, cert_data in certs.items():
cert = CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % name).first()
@ -54,7 +82,7 @@ def certificate_discovery(self: PrefilledMonitoredTask):
cert.certificate_data = cert_data
dirty = True
if name in private_keys:
if cert.key_data == private_keys[name]:
if cert.key_data != private_keys[name]:
cert.key_data = private_keys[name]
dirty = True
if dirty:

View File

@ -146,7 +146,7 @@ class TestCrypto(APITestCase):
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://localhost",
rsa_key=keypair,
signing_key=keypair,
)
response = self.client.get(
reverse(
@ -191,9 +191,12 @@ class TestCrypto(APITestCase):
with CONFIG.patch("cert_discovery_dir", temp_dir):
# pyright: reportGeneralTypeIssues=false
certificate_discovery() # pylint: disable=no-value-for-parameter
self.assertTrue(
CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo").exists()
)
keypair: CertificateKeyPair = CertificateKeyPair.objects.filter(
managed=MANAGED_DISCOVERED % "foo"
).first()
self.assertIsNotNone(keypair)
self.assertIsNotNone(keypair.certificate)
self.assertIsNotNone(keypair.private_key)
self.assertTrue(
CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo.bar").exists()
)

View File

@ -1,4 +1,6 @@
"""Events API Views"""
from json import loads
import django_filters
from django.db.models.aggregates import Count
from django.db.models.fields.json import KeyTextTransform
@ -12,6 +14,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer
from authentik.events.models import Event, EventAction
@ -110,13 +113,20 @@ class EventViewSet(ModelViewSet):
@extend_schema(
methods=["GET"],
responses={200: EventTopPerUserSerializer(many=True)},
filters=[],
parameters=[
OpenApiParameter(
"action",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
required=False,
),
OpenApiParameter(
"top_n",
type=OpenApiTypes.INT,
location=OpenApiParameter.QUERY,
required=False,
)
),
],
)
@action(detail=False, methods=["GET"], pagination_class=None)
@ -137,6 +147,40 @@ class EventViewSet(ModelViewSet):
.order_by("-counted_events")[:top_n]
)
@extend_schema(
methods=["GET"],
responses={200: CoordinateSerializer(many=True)},
filters=[],
parameters=[
OpenApiParameter(
"action",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
required=False,
),
OpenApiParameter(
"query",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
required=False,
),
],
)
@action(detail=False, methods=["GET"], pagination_class=None)
def per_month(self, request: Request):
"""Get the count of events per month"""
filtered_action = request.query_params.get("action", EventAction.LOGIN)
try:
query = loads(request.query_params.get("query", "{}"))
except ValueError:
return Response(status=400)
return Response(
get_objects_for_user(request.user, "authentik_events.view_event")
.filter(action=filtered_action)
.filter(**query)
.get_events_per_day()
)
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def actions(self, request: Request) -> Response:

View File

@ -15,12 +15,14 @@ from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import (
Event,
Notification,
NotificationSeverity,
NotificationTransport,
NotificationTransportError,
TransportMode,
)
from authentik.events.utils import get_user
class NotificationTransportSerializer(ModelSerializer):
@ -86,6 +88,12 @@ class NotificationTransportViewSet(UsedByMixin, ModelViewSet):
severity=NotificationSeverity.NOTICE,
body=f"Test Notification from transport {transport.name}",
user=request.user,
event=Event(
action="Test",
user=get_user(request.user),
app=self.__class__.__module__,
context={"foo": "bar"},
),
)
try:
response = NotificationTransportTestSerializer(

View File

@ -7,6 +7,7 @@ from typing import Optional, TypedDict
from geoip2.database import Reader
from geoip2.errors import GeoIP2Error
from geoip2.models import City
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.lib.config import CONFIG
@ -62,13 +63,17 @@ class GeoIPReader:
def city(self, ip_address: str) -> Optional[City]:
"""Wrapper for Reader.city"""
if not self.enabled:
return None
self.__check_expired()
try:
return self.__reader.city(ip_address)
except (GeoIP2Error, ValueError):
return None
with Hub.current.start_span(
op="authentik.events.geo.city",
description=ip_address,
):
if not self.enabled:
return None
self.__check_expired()
try:
return self.__reader.city(ip_address)
except (GeoIP2Error, ValueError):
return None
def city_dict(self, ip_address: str) -> Optional[GeoIPDict]:
"""Wrapper for self.city that returns a dict"""

View File

@ -314,169 +314,10 @@ class Migration(migrations.Migration):
old_name="user_json",
new_name="user",
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("sign_up", "Sign Up"),
("authorize_application", "Authorize Application"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
migrations.RemoveField(
model_name="event",
name="date",
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.CreateModel(
name="NotificationTransport",
fields=[
@ -610,68 +451,6 @@ class Migration(migrations.Migration):
help_text="Only send notification once, for example when sending a webhook into a chat channel.",
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.RunPython(
code=token_view_to_secret_view,
),
@ -688,76 +467,11 @@ class Migration(migrations.Migration):
migrations.RunPython(
code=update_expires,
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("email_sent", "Email Sent"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AddField(
model_name="event",
name="tenant",
field=models.JSONField(blank=True, default=authentik.events.models.default_tenant),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("system_exception", "System Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("email_sent", "Email Sent"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
@ -776,6 +490,7 @@ class Migration(migrations.Migration):
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("flow_execution", "Flow Execution"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),

View File

@ -1,12 +1,20 @@
"""authentik events models"""
import time
from collections import Counter
from datetime import timedelta
from inspect import getmodule, stack
from inspect import currentframe
from smtplib import SMTPException
from typing import TYPE_CHECKING, Optional, Type, Union
from uuid import uuid4
from django.conf import settings
from django.db import models
from django.db.models import Count, ExpressionWrapper, F
from django.db.models.fields import DurationField
from django.db.models.functions import ExtractHour
from django.db.models.functions.datetime import ExtractDay
from django.db.models.manager import Manager
from django.db.models.query import QuerySet
from django.http import HttpRequest
from django.http.request import QueryDict
from django.utils.timezone import now
@ -70,6 +78,7 @@ class EventAction(models.TextChoices):
IMPERSONATION_STARTED = "impersonation_started"
IMPERSONATION_ENDED = "impersonation_ended"
FLOW_EXECUTION = "flow_execution"
POLICY_EXECUTION = "policy_execution"
POLICY_EXCEPTION = "policy_exception"
PROPERTY_MAPPING_EXCEPTION = "property_mapping_exception"
@ -90,6 +99,72 @@ class EventAction(models.TextChoices):
CUSTOM_PREFIX = "custom_"
class EventQuerySet(QuerySet):
"""Custom events query set with helper functions"""
def get_events_per_hour(self) -> list[dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
date_from = now() - timedelta(days=1)
result = (
self.filter(created__gte=date_from)
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
.annotate(age_hours=ExtractHour("age"))
.values("age_hours")
.annotate(count=Count("pk"))
.order_by("age_hours")
)
data = Counter({int(d["age_hours"]): d["count"] for d in result})
results = []
_now = now()
for hour in range(0, -24, -1):
results.append(
{
"x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
"y_cord": data[hour * -1],
}
)
return results
def get_events_per_day(self) -> list[dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
date_from = now() - timedelta(weeks=4)
result = (
self.filter(created__gte=date_from)
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
.annotate(age_days=ExtractDay("age"))
.values("age_days")
.annotate(count=Count("pk"))
.order_by("age_days")
)
data = Counter({int(d["age_days"]): d["count"] for d in result})
results = []
_now = now()
for day in range(0, -30, -1):
results.append(
{
"x_cord": time.mktime((_now + timedelta(days=day)).timetuple()) * 1000,
"y_cord": data[day * -1],
}
)
return results
class EventManager(Manager):
"""Custom helper methods for Events"""
def get_queryset(self) -> QuerySet:
"""use custom queryset"""
return EventQuerySet(self.model, using=self._db)
def get_events_per_hour(self) -> list[dict[str, int]]:
"""Wrap method from queryset"""
return self.get_queryset().get_events_per_hour()
def get_events_per_day(self) -> list[dict[str, int]]:
"""Wrap method from queryset"""
return self.get_queryset().get_events_per_day()
class Event(ExpiringModel):
"""An individual Audit/Metrics/Notification/Error Event"""
@ -105,6 +180,8 @@ class Event(ExpiringModel):
# Shadow the expires attribute from ExpiringModel to override the default duration
expires = models.DateTimeField(default=default_event_duration)
objects = EventManager()
@staticmethod
def _get_app_from_request(request: HttpRequest) -> str:
if not isinstance(request, HttpRequest):
@ -115,14 +192,15 @@ class Event(ExpiringModel):
def new(
action: Union[str, EventAction],
app: Optional[str] = None,
_inspect_offset: int = 1,
**kwargs,
) -> "Event":
"""Create new Event instance from arguments. Instance is NOT saved."""
if not isinstance(action, EventAction):
action = EventAction.CUSTOM_PREFIX + action
if not app:
app = getmodule(stack()[_inspect_offset][0]).__name__
current = currentframe()
parent = current.f_back
app = parent.f_globals["__name__"]
cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs))
event = Event(action=action, app=app, context=cleaned_kwargs)
return event

View File

@ -46,7 +46,7 @@ class TaskResult:
def with_error(self, exc: Exception) -> "TaskResult":
"""Since errors might not always be pickle-able, set the traceback"""
self.messages.extend(exception_to_string(exc).splitlines())
self.messages.append(str(exc))
return self
@ -186,31 +186,21 @@ class MonitoredTask(Task):
raise NotImplementedError
class PrefilledMonitoredTask(MonitoredTask):
"""Subclass of MonitoredTask, but create entry in cache if task hasn't been run
Does not support UID"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
status = TaskInfo.by_name(self.__name__)
if status:
return
TaskInfo(
task_name=self.__name__,
task_description=self.__doc__,
result=TaskResult(TaskResultStatus.UNKNOWN, messages=[_("Task has not been run yet.")]),
task_call_module=self.__module__,
task_call_func=self.__name__,
# We don't have real values for these attributes but they cannot be null
start_timestamp=default_timer(),
finish_timestamp=default_timer(),
finish_time=datetime.now(),
).save(86400)
LOGGER.debug("prefilled task", task_name=self.__name__)
def run(self, *args, **kwargs):
raise NotImplementedError
for task in TaskInfo.all().values():
task.set_prom_metrics()
def prefill_task(func):
"""Ensure a task's details are always in cache, so it can always be triggered via API"""
status = TaskInfo.by_name(func.__name__)
if status:
return func
TaskInfo(
task_name=func.__name__,
task_description=func.__doc__,
result=TaskResult(TaskResultStatus.UNKNOWN, messages=[_("Task has not been run yet.")]),
task_call_module=func.__module__,
task_call_func=func.__name__,
# We don't have real values for these attributes but they cannot be null
start_timestamp=default_timer(),
finish_timestamp=default_timer(),
finish_time=datetime.now(),
).save(86400)
LOGGER.debug("prefilled task", task_name=func.__name__)
return func

View File

@ -90,7 +90,7 @@ class StageViewSet(
stages += list(configurable_stage.objects.all().order_by("name"))
matching_stages: list[dict] = []
for stage in stages:
user_settings = stage.ui_user_settings
user_settings = stage.ui_user_settings()
if not user_settings:
continue
user_settings.initial_data["object_uid"] = str(stage.pk)

View File

@ -0,0 +1,46 @@
# Generated by Django 3.2.9 on 2021-12-05 13:50
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0018_auto_20210330_1345_squashed_0028_alter_token_intent"),
(
"authentik_flows",
"0019_alter_flow_background_squashed_0024_alter_flow_compatibility_mode",
),
]
operations = [
migrations.CreateModel(
name="FlowToken",
fields=[
(
"token_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.token",
),
),
("_plan", models.TextField()),
(
"flow",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_flows.flow"
),
),
],
options={
"verbose_name": "Flow Token",
"verbose_name_plural": "Flow Tokens",
},
bases=("authentik_core.token",),
),
]

View File

@ -1,4 +1,6 @@
"""Flow models"""
from base64 import b64decode, b64encode
from pickle import dumps, loads # nosec
from typing import TYPE_CHECKING, Optional, Type
from uuid import uuid4
@ -9,11 +11,13 @@ from model_utils.managers import InheritanceManager
from rest_framework.serializers import BaseSerializer
from structlog.stdlib import get_logger
from authentik.core.models import Token
from authentik.core.types import UserSettingSerializer
from authentik.lib.models import InheritanceForeignKey, SerializerModel
from authentik.policies.models import PolicyBindingModel
if TYPE_CHECKING:
from authentik.flows.planner import FlowPlan
from authentik.flows.stage import StageView
LOGGER = get_logger()
@ -71,7 +75,6 @@ class Stage(SerializerModel):
"""Return component used to edit this object"""
raise NotImplementedError
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
"""Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or a challenge."""
@ -260,3 +263,30 @@ class ConfigurableStage(models.Model):
class Meta:
abstract = True
class FlowToken(Token):
"""Subclass of a standard Token, stores the currently active flow plan upon creation.
Can be used to later resume a flow."""
flow = models.ForeignKey(Flow, on_delete=models.CASCADE)
_plan = models.TextField()
@staticmethod
def pickle(plan) -> str:
"""Pickle into string"""
data = dumps(plan)
return b64encode(data).decode()
@property
def plan(self) -> "FlowPlan":
"""Load Flow plan from pickled version"""
return loads(b64decode(self._plan.encode())) # nosec
def __str__(self) -> str:
return f"Flow Token {super().__str__()}"
class Meta:
verbose_name = _("Flow Token")
verbose_name_plural = _("Flow Tokens")

View File

@ -24,6 +24,9 @@ PLAN_CONTEXT_SSO = "is_sso"
PLAN_CONTEXT_REDIRECT = "redirect"
PLAN_CONTEXT_APPLICATION = "application"
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(
"authentik_flows_cached",
"Cached flows",
@ -123,7 +126,9 @@ class FlowPlanner:
) -> FlowPlan:
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
and return ordered list"""
with Hub.current.start_span(op="flow.planner.plan") as span:
with Hub.current.start_span(
op="authentik.flow.planner.plan", description=self.flow.slug
) as span:
span: Span
span.set_data("flow", self.flow)
span.set_data("request", request)
@ -178,7 +183,8 @@ class FlowPlanner:
"""Build flow plan by checking each stage in their respective
order and checking the applied policies"""
with Hub.current.start_span(
op="flow.planner.build_plan"
op="authentik.flow.planner.build_plan",
description=self.flow.slug,
) as span, HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug).time():
span: Span
span.set_data("flow", self.flow)

View File

@ -6,6 +6,7 @@ from django.http.response import HttpResponse
from django.urls import reverse
from django.views.generic.base import View
from rest_framework.request import Request
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.core.models import DEFAULT_AVATAR, User
@ -94,8 +95,16 @@ class ChallengeStageView(StageView):
keep_context=keep_context,
)
return self.executor.restart_flow(keep_context)
return self.challenge_invalid(challenge)
return self.challenge_valid(challenge)
with Hub.current.start_span(
op="authentik.flow.stage.challenge_invalid",
description=self.__class__.__name__,
):
return self.challenge_invalid(challenge)
with Hub.current.start_span(
op="authentik.flow.stage.challenge_valid",
description=self.__class__.__name__,
):
return self.challenge_valid(challenge)
def format_title(self) -> str:
"""Allow usage of placeholder in flow title."""
@ -104,7 +113,11 @@ class ChallengeStageView(StageView):
}
def _get_challenge(self, *args, **kwargs) -> Challenge:
challenge = self.get_challenge(*args, **kwargs)
with Hub.current.start_span(
op="authentik.flow.stage.get_challenge",
description=self.__class__.__name__,
):
challenge = self.get_challenge(*args, **kwargs)
if "flow_info" not in challenge.initial_data:
flow_info = ContextualFlowInfo(
data={

View File

@ -32,7 +32,7 @@ class TestFlowsAPI(APITestCase):
def test_models(self):
"""Test that ui_user_settings returns none"""
self.assertIsNone(Stage().ui_user_settings)
self.assertIsNone(Stage().ui_user_settings())
def test_api_serializer(self):
"""Test that stage serializer returns the correct type"""

View File

@ -23,7 +23,7 @@ def model_tester_factory(test_model: Type[Stage]) -> Callable:
model_class = test_model()
self.assertTrue(issubclass(model_class.type, StageView))
self.assertIsNotNone(test_model.component)
_ = model_class.ui_user_settings
_ = model_class.ui_user_settings()
return tester

View File

@ -19,6 +19,8 @@ from drf_spectacular.utils import OpenApiParameter, PolymorphicProxySerializer,
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from sentry_sdk import capture_exception
from sentry_sdk.api import set_tag
from sentry_sdk.hub import Hub
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.models import USER_ATTRIBUTE_DEBUG
@ -34,8 +36,16 @@ from authentik.flows.challenge import (
WithUserInfoChallenge,
)
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, FlowStageBinding, Stage
from authentik.flows.models import (
ConfigurableStage,
Flow,
FlowDesignation,
FlowStageBinding,
FlowToken,
Stage,
)
from authentik.flows.planner import (
PLAN_CONTEXT_IS_RESTORED,
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_REDIRECT,
FlowPlan,
@ -55,6 +65,7 @@ SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre"
SESSION_KEY_GET = "authentik_flows_get"
SESSION_KEY_POST = "authentik_flows_post"
SESSION_KEY_HISTORY = "authentik_flows_history"
QS_KEY_TOKEN = "flow_token" # nosec
def challenge_types():
@ -117,6 +128,7 @@ class FlowExecutorView(APIView):
super().setup(request, flow_slug=flow_slug)
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
self._logger = get_logger().bind(flow_slug=flow_slug)
set_tag("authentik.flow", self.flow.slug)
def handle_invalid_flow(self, exc: BaseException) -> HttpResponse:
"""When a flow is non-applicable check if user is on the correct domain"""
@ -127,71 +139,100 @@ class FlowExecutorView(APIView):
message = exc.__doc__ if exc.__doc__ else str(exc)
return self.stage_invalid(error_message=message)
def _check_flow_token(self, get_params: QueryDict):
"""Check if the user is using a flow token to restore a plan"""
tokens = FlowToken.filter_not_expired(key=get_params[QS_KEY_TOKEN])
if not tokens.exists():
return False
token: FlowToken = tokens.first()
try:
plan = token.plan
except (AttributeError, EOFError, ImportError, IndexError) as exc:
LOGGER.warning("f(exec): Failed to restore token plan", exc=exc)
finally:
token.delete()
if not isinstance(plan, FlowPlan):
return None
plan.context[PLAN_CONTEXT_IS_RESTORED] = True
self._logger.debug("f(exec): restored flow plan from token", plan=plan)
return plan
# pylint: disable=unused-argument, too-many-return-statements
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
# Early check if there's an active Plan for the current session
if SESSION_KEY_PLAN in self.request.session:
self.plan = self.request.session[SESSION_KEY_PLAN]
if self.plan.flow_pk != self.flow.pk.hex:
self._logger.warning(
"f(exec): Found existing plan for other flow, deleting plan",
)
# Existing plan is deleted from session and instance
self.plan = None
self.cancel()
self._logger.debug("f(exec): Continuing existing plan")
with Hub.current.start_span(
op="authentik.flow.executor.dispatch", description=self.flow.slug
) as span:
span.set_data("authentik Flow", self.flow.slug)
get_params = QueryDict(request.GET.get("query", ""))
if QS_KEY_TOKEN in get_params:
plan = self._check_flow_token(get_params)
if plan:
self.request.session[SESSION_KEY_PLAN] = plan
# Early check if there's an active Plan for the current session
if SESSION_KEY_PLAN in self.request.session:
self.plan = self.request.session[SESSION_KEY_PLAN]
if self.plan.flow_pk != self.flow.pk.hex:
self._logger.warning(
"f(exec): Found existing plan for other flow, deleting plan",
)
# Existing plan is deleted from session and instance
self.plan = None
self.cancel()
self._logger.debug("f(exec): Continuing existing plan")
# Don't check session again as we've either already loaded the plan or we need to plan
if not self.plan:
request.session[SESSION_KEY_HISTORY] = []
self._logger.debug("f(exec): No active Plan found, initiating planner")
# Don't check session again as we've either already loaded the plan or we need to plan
if not self.plan:
request.session[SESSION_KEY_HISTORY] = []
self._logger.debug("f(exec): No active Plan found, initiating planner")
try:
self.plan = self._initiate_plan()
except FlowNonApplicableException as exc:
self._logger.warning("f(exec): Flow not applicable to current user", exc=exc)
return to_stage_response(self.request, self.handle_invalid_flow(exc))
except EmptyFlowException as exc:
self._logger.warning("f(exec): Flow is empty", exc=exc)
# To match behaviour with loading an empty flow plan from cache,
# we don't show an error message here, but rather call _flow_done()
return self._flow_done()
# Initial flow request, check if we have an upstream query string passed in
request.session[SESSION_KEY_GET] = get_params
# We don't save the Plan after getting the next stage
# as it hasn't been successfully passed yet
try:
self.plan = self._initiate_plan()
except FlowNonApplicableException as exc:
self._logger.warning("f(exec): Flow not applicable to current user", exc=exc)
return to_stage_response(self.request, self.handle_invalid_flow(exc))
except EmptyFlowException as exc:
self._logger.warning("f(exec): Flow is empty", exc=exc)
# To match behaviour with loading an empty flow plan from cache,
# we don't show an error message here, but rather call _flow_done()
# This is the first time we actually access any attribute on the selected plan
# if the cached plan is from an older version, it might have different attributes
# in which case we just delete the plan and invalidate everything
next_binding = self.plan.next(self.request)
except Exception as exc: # pylint: disable=broad-except
self._logger.warning(
"f(exec): found incompatible flow plan, invalidating run", exc=exc
)
keys = cache.keys("flow_*")
cache.delete_many(keys)
return self.stage_invalid()
if not next_binding:
self._logger.debug("f(exec): no more stages, flow is done.")
return self._flow_done()
# Initial flow request, check if we have an upstream query string passed in
request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", ""))
# We don't save the Plan after getting the next stage
# as it hasn't been successfully passed yet
try:
# This is the first time we actually access any attribute on the selected plan
# if the cached plan is from an older version, it might have different attributes
# in which case we just delete the plan and invalidate everything
next_binding = self.plan.next(self.request)
except Exception as exc: # pylint: disable=broad-except
self._logger.warning("f(exec): found incompatible flow plan, invalidating run", exc=exc)
keys = cache.keys("flow_*")
cache.delete_many(keys)
return self.stage_invalid()
if not next_binding:
self._logger.debug("f(exec): no more stages, flow is done.")
return self._flow_done()
self.current_binding = next_binding
self.current_stage = next_binding.stage
self._logger.debug(
"f(exec): Current stage",
current_stage=self.current_stage,
flow_slug=self.flow.slug,
)
try:
stage_cls = self.current_stage.type
except NotImplementedError as exc:
self._logger.debug("Error getting stage type", exc=exc)
return self.stage_invalid()
self.current_stage_view = stage_cls(self)
self.current_stage_view.args = self.args
self.current_stage_view.kwargs = self.kwargs
self.current_stage_view.request = request
try:
return super().dispatch(request)
except InvalidStageError as exc:
return self.stage_invalid(str(exc))
self.current_binding = next_binding
self.current_stage = next_binding.stage
self._logger.debug(
"f(exec): Current stage",
current_stage=self.current_stage,
flow_slug=self.flow.slug,
)
try:
stage_cls = self.current_stage.type
except NotImplementedError as exc:
self._logger.debug("Error getting stage type", exc=exc)
return self.stage_invalid()
self.current_stage_view = stage_cls(self)
self.current_stage_view.args = self.args
self.current_stage_view.kwargs = self.kwargs
self.current_stage_view.request = request
try:
return super().dispatch(request)
except InvalidStageError as exc:
return self.stage_invalid(str(exc))
def handle_exception(self, exc: Exception) -> HttpResponse:
"""Handle exception in stage execution"""
@ -233,8 +274,15 @@ class FlowExecutorView(APIView):
stage=self.current_stage,
)
try:
stage_response = self.current_stage_view.get(request, *args, **kwargs)
return to_stage_response(request, stage_response)
with Hub.current.start_span(
op="authentik.flow.executor.stage",
description=class_to_path(self.current_stage_view.__class__),
) as span:
span.set_data("Method", "GET")
span.set_data("authentik Stage", self.current_stage_view)
span.set_data("authentik Flow", self.flow.slug)
stage_response = self.current_stage_view.get(request, *args, **kwargs)
return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except
return self.handle_exception(exc)
@ -270,8 +318,15 @@ class FlowExecutorView(APIView):
stage=self.current_stage,
)
try:
stage_response = self.current_stage_view.post(request, *args, **kwargs)
return to_stage_response(request, stage_response)
with Hub.current.start_span(
op="authentik.flow.executor.stage",
description=class_to_path(self.current_stage_view.__class__),
) as span:
span.set_data("Method", "POST")
span.set_data("authentik Stage", self.current_stage_view)
span.set_data("authentik Flow", self.flow.slug)
stage_response = self.current_stage_view.post(request, *args, **kwargs)
return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except
return self.handle_exception(exc)
@ -316,6 +371,12 @@ class FlowExecutorView(APIView):
NEXT_ARG_NAME, "authentik_core:root-redirect"
)
self.cancel()
Event.new(
action=EventAction.FLOW_EXECUTION,
flow=self.flow,
designation=self.flow.designation,
successful=True,
).from_http(self.request)
return to_stage_response(self.request, redirect_with_qs(next_param))
def stage_ok(self) -> HttpResponse:

View File

@ -87,9 +87,7 @@ class FlowInspectorView(APIView):
@extend_schema(
responses={
200: FlowInspectionSerializer(),
400: OpenApiResponse(
description="No flow plan in session."
), # This error can be raised by the email stage
400: OpenApiResponse(description="No flow plan in session."),
},
request=OpenApiTypes.NONE,
operation_id="flows_inspector_get",
@ -106,7 +104,10 @@ class FlowInspectorView(APIView):
if SESSION_KEY_PLAN in request.session:
current_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
else:
current_plan = request.session[SESSION_KEY_HISTORY][-1]
try:
current_plan = request.session[SESSION_KEY_HISTORY][-1]
except IndexError:
return Response(status=400)
is_completed = True
current_serializer = FlowInspectorPlanSerializer(
instance=current_plan, context={"request": request}

View File

@ -20,7 +20,6 @@ web:
listen: 0.0.0.0:9000
listen_tls: 0.0.0.0:9443
listen_metrics: 0.0.0.0:9300
load_local_files: false
outpost_port_offset: 0
redis:
@ -65,7 +64,7 @@ outposts:
# %(type)s: Outpost type; proxy, ldap, etc
# %(version)s: Current version; 2021.4.1
# %(build_hash)s: Build hash if you're running a beta version
container_image_base: goauthentik.io/%(type)s:%(version)s
container_image_base: ghcr.io/goauthentik/%(type)s:%(version)s
cookie_domain: null
disable_update_check: false

View File

@ -80,8 +80,9 @@ class BaseEvaluator:
"""Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
If any exception is raised during execution, it is raised.
The result is returned without any type-checking."""
with Hub.current.start_span(op="lib.evaluator.evaluate") as span:
with Hub.current.start_span(op="authentik.lib.evaluator.evaluate") as span:
span: Span
span.description = self._filename
span.set_data("expression", expression_source)
param_keys = self._context.keys()
try:

View File

@ -108,6 +108,9 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
"multiprocessing",
"django_redis",
"django.security.DisallowedHost",
"django_redis.cache",
"celery.backends.redis",
"celery.worker",
]:
return None
LOGGER.debug("sending event to sentry", exc=exc_value, source_logger=event.get("logger", None))

View File

@ -4,6 +4,7 @@ from typing import Any, Optional
from django.http import HttpRequest
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__
@ -52,6 +53,12 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
fake_ip=fake_ip,
)
return None
# Update sentry scope to include correct IP
user = Hub.current.scope._user
if not user:
user = {}
user["ip_address"] = fake_ip
Hub.current.scope.set_user(user)
return fake_ip

View File

@ -2,12 +2,18 @@
from django.db import DatabaseError
from authentik.core.tasks import CELERY_APP
from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
from authentik.events.monitored_tasks import (
MonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
from authentik.managed.manager import ObjectManager
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
def managed_reconcile(self: PrefilledMonitoredTask):
@CELERY_APP.task(bind=True, base=MonitoredTask)
@prefill_task
def managed_reconcile(self: MonitoredTask):
"""Run ObjectManager to ensure objects are up-to-date"""
try:
ObjectManager().run()

View File

@ -1,6 +1,8 @@
"""Outpost API Views"""
from dacite.core import from_dict
from dacite.exceptions import DaciteError
from django_filters.filters import ModelMultipleChoiceFilter
from django_filters.filterset import FilterSet
from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, DateTimeField
@ -99,16 +101,31 @@ class OutpostHealthSerializer(PassiveSerializer):
version_outdated = BooleanField(read_only=True)
class OutpostFilter(FilterSet):
"""Filter for Outposts"""
providers_by_pk = ModelMultipleChoiceFilter(
field_name="providers",
queryset=Provider.objects.all(),
)
class Meta:
model = Outpost
fields = {
"providers": ["isnull"],
"name": ["iexact", "icontains"],
"service_connection__name": ["iexact", "icontains"],
"managed": ["iexact", "icontains"],
}
class OutpostViewSet(UsedByMixin, ModelViewSet):
"""Outpost Viewset"""
queryset = Outpost.objects.all()
serializer_class = OutpostSerializer
filterset_fields = {
"providers": ["isnull"],
"name": ["iexact", "icontains"],
"service_connection__name": ["iexact", "icontains"],
}
filterset_class = OutpostFilter
search_fields = [
"name",
"providers__name",

View File

@ -9,7 +9,7 @@ from dacite import from_dict
from dacite.data import Data
from guardian.shortcuts import get_objects_for_user
from prometheus_client import Gauge
from structlog.stdlib import get_logger
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.channels import AuthJsonConsumer
from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState
@ -23,8 +23,6 @@ GAUGE_OUTPOSTS_LAST_UPDATE = Gauge(
["outpost", "uid", "version"],
)
LOGGER = get_logger()
class WebsocketMessageInstruction(IntEnum):
"""Commands which can be triggered over Websocket"""
@ -51,6 +49,7 @@ class OutpostConsumer(AuthJsonConsumer):
"""Handler for Outposts that connect over websockets for health checks and live updates"""
outpost: Optional[Outpost] = None
logger: BoundLogger
last_uid: Optional[str] = None
@ -59,11 +58,20 @@ class OutpostConsumer(AuthJsonConsumer):
def connect(self):
super().connect()
uuid = self.scope["url_route"]["kwargs"]["pk"]
outpost = get_objects_for_user(self.user, "authentik_outposts.view_outpost").filter(pk=uuid)
if not outpost.exists():
outpost = (
get_objects_for_user(self.user, "authentik_outposts.view_outpost")
.filter(pk=uuid)
.first()
)
if not outpost:
raise DenyConnection()
self.accept()
self.outpost = outpost.first()
self.logger = get_logger().bind(outpost=outpost)
try:
self.accept()
except RuntimeError as exc:
self.logger.warning("runtime error during accept", exc=exc)
raise DenyConnection()
self.outpost = outpost
self.last_uid = self.channel_name
# pylint: disable=unused-argument
@ -78,9 +86,8 @@ class OutpostConsumer(AuthJsonConsumer):
uid=self.last_uid,
expected=self.outpost.config.kubernetes_replicas,
).dec()
LOGGER.debug(
self.logger.debug(
"removed outpost instance from cache",
outpost=self.outpost,
instance_uuid=self.last_uid,
)
@ -103,9 +110,8 @@ class OutpostConsumer(AuthJsonConsumer):
uid=self.last_uid,
expected=self.outpost.config.kubernetes_replicas,
).inc()
LOGGER.debug(
self.logger.debug(
"added outpost instance to cache",
outpost=self.outpost,
instance_uuid=self.last_uid,
)
self.first_msg = True

View File

@ -24,6 +24,8 @@ class DockerController(BaseController):
def __init__(self, outpost: Outpost, connection: DockerServiceConnection) -> None:
super().__init__(outpost, connection)
if outpost.managed == MANAGED_OUTPOST:
return
try:
self.client = connection.client()
except ServiceConnectionInvalid as exc:
@ -108,7 +110,7 @@ class DockerController(BaseController):
image = self.get_container_image()
try:
self.client.images.pull(image)
except DockerException:
except DockerException: # pragma: no cover
image = f"goauthentik.io/{self.outpost.type}:latest"
self.client.images.pull(image)
return image
@ -142,7 +144,7 @@ class DockerController(BaseController):
True,
)
def _migrate_container_name(self):
def _migrate_container_name(self): # pragma: no cover
"""Migrate 2021.9 to 2021.10+"""
old_name = f"authentik-proxy-{self.outpost.uuid.hex}"
try:
@ -167,7 +169,7 @@ class DockerController(BaseController):
# Check if the container is out of date, delete it and retry
if len(container.image.tags) > 0:
should_image = self.try_pull_image()
if should_image not in container.image.tags:
if should_image not in container.image.tags: # pragma: no cover
self.logger.info(
"Container has mismatched image, re-creating...",
has=container.image.tags,
@ -225,12 +227,14 @@ class DockerController(BaseController):
raise ControllerException(str(exc)) from exc
def down(self):
if self.outpost.managed != MANAGED_OUTPOST:
if self.outpost.managed == MANAGED_OUTPOST:
return
try:
container, _ = self._get_container()
if container.status == "running":
self.logger.info("Stopping container.")
container.kill()
self.logger.info("Removing container.")
container.remove(force=True)
except DockerException as exc:
raise ControllerException(str(exc)) from exc

View File

@ -20,6 +20,11 @@ if TYPE_CHECKING:
T = TypeVar("T", V1Pod, V1Deployment)
def get_version() -> str:
"""Wrapper for __version__ to make testing easier"""
return __version__
class KubernetesObjectReconciler(Generic[T]):
"""Base Kubernetes Reconciler, handles the basic logic."""
@ -146,13 +151,13 @@ class KubernetesObjectReconciler(Generic[T]):
return V1ObjectMeta(
namespace=self.namespace,
labels={
"app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
"app.kubernetes.io/instance": slugify(self.controller.outpost.name),
"app.kubernetes.io/version": __version__,
"app.kubernetes.io/managed-by": "goauthentik.io",
"goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
"goauthentik.io/outpost-type": str(self.controller.outpost.type),
"app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
"app.kubernetes.io/version": get_version(),
"goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
"goauthentik.io/outpost-type": str(self.controller.outpost.type),
"goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
},
**kwargs,
)

View File

@ -45,6 +45,7 @@ from authentik.lib.utils.errors import exception_to_string
from authentik.managed.models import ManagedModel
from authentik.outposts.controllers.k8s.utils import get_namespace
from authentik.outposts.docker_tls import DockerInlineTLS
from authentik.tenants.models import Tenant
OUR_VERSION = parse(__version__)
OUTPOST_HELLO_INTERVAL = 10
@ -385,7 +386,8 @@ class Outpost(ManagedModel):
user.user_permissions.add(permission.first())
LOGGER.debug(
"Updated service account's permissions",
perms=UserObjectPermission.objects.filter(user=user),
obj_perms=UserObjectPermission.objects.filter(user=user),
perms=user.user_permissions.all(),
)
@property
@ -401,6 +403,7 @@ class Outpost(ManagedModel):
user = users.first()
user.attributes[USER_ATTRIBUTE_SA] = True
user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
user.name = f"Outpost {self.name} Service-Account"
user.save()
if should_create_user:
self.build_user_permissions(user)
@ -448,6 +451,10 @@ class Outpost(ManagedModel):
objects.extend(provider.get_required_objects())
else:
objects.append(provider)
if self.managed:
for tenant in Tenant.objects.filter(web_certificate__isnull=False):
objects.append(tenant)
objects.append(tenant.web_certificate)
return objects
def __str__(self) -> str:
@ -480,6 +487,8 @@ class OutpostState:
def for_outpost(outpost: Outpost) -> list["OutpostState"]:
"""Get all states for an outpost"""
keys = cache.keys(f"{outpost.state_cache_prefix}_*")
if not keys:
return []
states = []
for key in keys:
instance_uid = key.replace(f"{outpost.state_cache_prefix}_", "")

View File

@ -10,6 +10,7 @@ from authentik.crypto.models import CertificateKeyPair
from authentik.lib.utils.reflection import class_to_path
from authentik.outposts.models import Outpost, OutpostServiceConnection
from authentik.outposts.tasks import CACHE_KEY_OUTPOST_DOWN, outpost_controller, outpost_post_save
from authentik.tenants.models import Tenant
LOGGER = get_logger()
UPDATE_TRIGGERING_MODELS = (
@ -17,6 +18,7 @@ UPDATE_TRIGGERING_MODELS = (
OutpostServiceConnection,
Provider,
CertificateKeyPair,
Tenant,
)

View File

@ -19,9 +19,9 @@ from structlog.stdlib import get_logger
from authentik.events.monitored_tasks import (
MonitoredTask,
PrefilledMonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
from authentik.lib.utils.reflection import path_to_class
from authentik.outposts.controllers.base import BaseController, ControllerException
@ -75,8 +75,9 @@ def outpost_service_connection_state(connection_pk: Any):
cache.set(connection.state_key, state, timeout=None)
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
def outpost_service_connection_monitor(self: PrefilledMonitoredTask):
@CELERY_APP.task(bind=True, base=MonitoredTask)
@prefill_task
def outpost_service_connection_monitor(self: MonitoredTask):
"""Regularly check the state of Outpost Service Connections"""
connections = OutpostServiceConnection.objects.all()
for connection in connections.iterator():
@ -104,9 +105,12 @@ def outpost_controller(
logs = []
if from_cache:
outpost: Outpost = cache.get(CACHE_KEY_OUTPOST_DOWN % outpost_pk)
LOGGER.debug("Getting outpost from cache to delete")
else:
outpost: Outpost = Outpost.objects.filter(pk=outpost_pk).first()
LOGGER.debug("Getting outpost from DB")
if not outpost:
LOGGER.warning("No outpost")
return
self.set_uid(slugify(outpost.name))
try:
@ -124,8 +128,9 @@ def outpost_controller(
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, logs))
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
def outpost_token_ensurer(self: PrefilledMonitoredTask):
@CELERY_APP.task(bind=True, base=MonitoredTask)
@prefill_task
def outpost_token_ensurer(self: MonitoredTask):
"""Periodically ensure that all Outposts have valid Service Accounts
and Tokens"""
all_outposts = Outpost.objects.all()

View File

@ -0,0 +1,124 @@
"""Docker controller tests"""
from django.test import TestCase
from docker.models.containers import Container
from authentik.managed.manager import ObjectManager
from authentik.outposts.controllers.base import ControllerException
from authentik.outposts.controllers.docker import DockerController
from authentik.outposts.managed import MANAGED_OUTPOST
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostType
from authentik.providers.proxy.controllers.docker import ProxyDockerController
class DockerControllerTests(TestCase):
"""Docker controller tests"""
def setUp(self) -> None:
self.outpost = Outpost.objects.create(
name="test",
type=OutpostType.PROXY,
)
self.integration = DockerServiceConnection(name="test")
ObjectManager().run()
def test_init_managed(self):
"""Docker controller shouldn't do anything for managed outpost"""
controller = DockerController(
Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
)
self.assertIsNone(controller.up())
self.assertIsNone(controller.down())
def test_init_invalid(self):
"""Ensure init fails with invalid client"""
with self.assertRaises(ControllerException):
DockerController(self.outpost, self.integration)
def test_env_valid(self):
"""Test environment check"""
controller = DockerController(
Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
)
env = [f"{key}={value}" for key, value in controller._get_env().items()]
container = Container(attrs={"Config": {"Env": env}})
self.assertFalse(controller._comp_env(container))
def test_env_invalid(self):
"""Test environment check"""
controller = DockerController(
Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
)
container = Container(attrs={"Config": {"Env": []}})
self.assertTrue(controller._comp_env(container))
def test_label_valid(self):
"""Test label check"""
controller = DockerController(
Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
)
container = Container(attrs={"Config": {"Labels": controller._get_labels()}})
self.assertFalse(controller._comp_labels(container))
def test_label_invalid(self):
"""Test label check"""
controller = DockerController(
Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
)
container = Container(attrs={"Config": {"Labels": {}}})
self.assertTrue(controller._comp_labels(container))
container = Container(attrs={"Config": {"Labels": {"io.goauthentik.outpost-uuid": "foo"}}})
self.assertTrue(controller._comp_labels(container))
def test_port_valid(self):
"""Test port check"""
controller = ProxyDockerController(
Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
)
container = Container(
attrs={
"NetworkSettings": {
"Ports": {
"9000/tcp": [{"HostIp": "", "HostPort": "9000"}],
"9443/tcp": [{"HostIp": "", "HostPort": "9443"}],
}
},
"State": "",
}
)
with self.settings(TEST=False):
self.assertFalse(controller._comp_ports(container))
container.attrs["State"] = "running"
self.assertFalse(controller._comp_ports(container))
def test_port_invalid(self):
"""Test port check"""
controller = ProxyDockerController(
Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
)
container_no_ports = Container(
attrs={"NetworkSettings": {"Ports": None}, "State": "running"}
)
container_missing_port = Container(
attrs={
"NetworkSettings": {
"Ports": {
"9443/tcp": [{"HostIp": "", "HostPort": "9443"}],
}
},
"State": "running",
}
)
container_mismatched_host = Container(
attrs={
"NetworkSettings": {
"Ports": {
"9443/tcp": [{"HostIp": "", "HostPort": "123"}],
}
},
"State": "running",
}
)
with self.settings(TEST=False):
self.assertFalse(controller._comp_ports(container_no_ports))
self.assertTrue(controller._comp_ports(container_missing_port))
self.assertTrue(controller._comp_ports(container_mismatched_host))

View File

@ -90,7 +90,8 @@ class PolicyEngine:
def build(self) -> "PolicyEngine":
"""Build wrapper which monitors performance"""
with Hub.current.start_span(
op="policy.engine.build"
op="authentik.policy.engine.build",
description=self.__pbm,
) as span, HIST_POLICIES_BUILD_TIME.labels(
object_name=self.__pbm,
object_type=f"{self.__pbm._meta.app_label}.{self.__pbm._meta.model_name}",

View File

@ -66,6 +66,7 @@ class Migration(migrations.Migration):
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("flow_execution", "Flow Execution"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),

View File

@ -11,6 +11,8 @@ from authentik.flows.planner import PLAN_CONTEXT_SSO
from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.utils.http import get_client_ip
from authentik.policies.exceptions import PolicyException
from authentik.policies.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess
from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger()
@ -31,6 +33,7 @@ class PolicyEvaluator(BaseEvaluator):
self._context["ak_logger"] = get_logger(policy_name)
self._context["ak_message"] = self.expr_func_message
self._context["ak_user_has_authenticator"] = self.expr_func_user_has_authenticator
self._context["ak_call_policy"] = self.expr_func_call_policy
self._context["ip_address"] = ip_address
self._context["ip_network"] = ip_network
self._filename = policy_name or "PolicyEvaluator"
@ -39,6 +42,16 @@ class PolicyEvaluator(BaseEvaluator):
"""Wrapper to append to messages list, which is returned with PolicyResult"""
self._messages.append(message)
def expr_func_call_policy(self, name: str, **kwargs) -> PolicyResult:
"""Call policy by name, with current request"""
policy = Policy.objects.filter(name=name).select_subclasses().first()
if not policy:
raise ValueError(f"Policy '{name}' not found.")
req: PolicyRequest = self._context["request"]
req.context.update(kwargs)
proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
return proc.profiling_wrapper()
def expr_func_user_has_authenticator(
self, user: User, device_type: Optional[str] = None
) -> bool:

View File

@ -74,4 +74,4 @@ class TestExpressionPolicyAPI(APITestCase):
expr = "return True"
self.assertEqual(ExpressionPolicySerializer().validate_expression(expr), expr)
with self.assertRaises(ValidationError):
print(ExpressionPolicySerializer().validate_expression("/"))
ExpressionPolicySerializer().validate_expression("/")

View File

@ -13,6 +13,7 @@ class PasswordPolicySerializer(PolicySerializer):
model = PasswordPolicy
fields = PolicySerializer.Meta.fields + [
"password_field",
"amount_digits",
"amount_uppercase",
"amount_lowercase",
"amount_symbols",

View File

@ -0,0 +1,38 @@
# Generated by Django 4.0 on 2021-12-18 14:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_policies_password", "0002_passwordpolicy_password_field"),
]
operations = [
migrations.AddField(
model_name="passwordpolicy",
name="amount_digits",
field=models.PositiveIntegerField(default=0),
),
migrations.AlterField(
model_name="passwordpolicy",
name="amount_lowercase",
field=models.PositiveIntegerField(default=0),
),
migrations.AlterField(
model_name="passwordpolicy",
name="amount_symbols",
field=models.PositiveIntegerField(default=0),
),
migrations.AlterField(
model_name="passwordpolicy",
name="amount_uppercase",
field=models.PositiveIntegerField(default=0),
),
migrations.AlterField(
model_name="passwordpolicy",
name="length_min",
field=models.PositiveIntegerField(default=0),
),
]

View File

@ -13,6 +13,7 @@ from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
LOGGER = get_logger()
RE_LOWER = re.compile("[a-z]")
RE_UPPER = re.compile("[A-Z]")
RE_DIGITS = re.compile("[0-9]")
class PasswordPolicy(Policy):
@ -23,10 +24,11 @@ class PasswordPolicy(Policy):
help_text=_("Field key to check, field keys defined in Prompt stages are available."),
)
amount_uppercase = models.IntegerField(default=0)
amount_lowercase = models.IntegerField(default=0)
amount_symbols = models.IntegerField(default=0)
length_min = models.IntegerField(default=0)
amount_digits = models.PositiveIntegerField(default=0)
amount_uppercase = models.PositiveIntegerField(default=0)
amount_lowercase = models.PositiveIntegerField(default=0)
amount_symbols = models.PositiveIntegerField(default=0)
length_min = models.PositiveIntegerField(default=0)
symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
error_message = models.TextField()
@ -40,6 +42,7 @@ class PasswordPolicy(Policy):
def component(self) -> str:
return "ak-policy-password-form"
# pylint: disable=too-many-return-statements
def passes(self, request: PolicyRequest) -> PolicyResult:
if (
self.password_field not in request.context
@ -62,6 +65,9 @@ class PasswordPolicy(Policy):
LOGGER.debug("password failed", reason="length")
return PolicyResult(False, self.error_message)
if self.amount_digits > 0 and len(RE_DIGITS.findall(password)) < self.amount_digits:
LOGGER.debug("password failed", reason="amount_digits")
return PolicyResult(False, self.error_message)
if self.amount_lowercase > 0 and len(RE_LOWER.findall(password)) < self.amount_lowercase:
LOGGER.debug("password failed", reason="amount_lowercase")
return PolicyResult(False, self.error_message)

View File

@ -13,6 +13,7 @@ class TestPasswordPolicy(TestCase):
def setUp(self) -> None:
self.policy = PasswordPolicy.objects.create(
name="test_false",
amount_digits=1,
amount_uppercase=1,
amount_lowercase=2,
amount_symbols=3,
@ -38,7 +39,7 @@ class TestPasswordPolicy(TestCase):
def test_failed_lowercase(self):
"""not enough lowercase"""
request = PolicyRequest(get_anonymous_user())
request.context["password"] = "TTTTTTTTTTTTTTTTTTTTTTTe" # nosec
request.context["password"] = "1TTTTTTTTTTTTTTTTTTTTTTe" # nosec
result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages, ("test message",))
@ -46,15 +47,23 @@ class TestPasswordPolicy(TestCase):
def test_failed_uppercase(self):
"""not enough uppercase"""
request = PolicyRequest(get_anonymous_user())
request.context["password"] = "tttttttttttttttttttttttE" # nosec
request.context["password"] = "1tttttttttttttttttttttE" # nosec
result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages, ("test message",))
def test_failed_symbols(self):
"""not enough uppercase"""
"""not enough symbols"""
request = PolicyRequest(get_anonymous_user())
request.context["password"] = "TETETETETETETETETETETETETe!!!" # nosec
request.context["password"] = "1ETETETETETETETETETETETETe!!!" # nosec
result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages, ("test message",))
def test_failed_digits(self):
"""not enough digits"""
request = PolicyRequest(get_anonymous_user())
request.context["password"] = "TETETETETETETETETETETE1e!!!" # nosec
result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages, ("test message",))
@ -62,7 +71,7 @@ class TestPasswordPolicy(TestCase):
def test_true(self):
"""Positive password case"""
request = PolicyRequest(get_anonymous_user())
request.context["password"] = generate_key() + "ee!!!" # nosec
request.context["password"] = generate_key() + "1ee!!!" # nosec
result: PolicyResult = self.policy.passes(request)
self.assertTrue(result.passing)
self.assertEqual(result.messages, tuple())

View File

@ -127,10 +127,10 @@ class PolicyProcess(PROCESS_CLASS):
)
return policy_result
def run(self): # pragma: no cover
"""Task wrapper to run policy checking"""
def profiling_wrapper(self):
"""Run with profiling enabled"""
with Hub.current.start_span(
op="policy.process.execute",
op="authentik.policy.process.execute",
) as span, HIST_POLICIES_EXECUTION_TIME.labels(
binding_order=self.binding.order,
binding_target_type=self.binding.target_type,
@ -142,8 +142,12 @@ class PolicyProcess(PROCESS_CLASS):
span: Span
span.set_data("policy", self.binding.policy)
span.set_data("request", self.request)
try:
self.connection.send(self.execute())
except Exception as exc: # pylint: disable=broad-except
LOGGER.warning(str(exc))
self.connection.send(PolicyResult(False, str(exc)))
return self.execute()
def run(self): # pragma: no cover
"""Task wrapper to run policy checking"""
try:
self.connection.send(self.profiling_wrapper())
except Exception as exc: # pylint: disable=broad-except
LOGGER.warning(str(exc))
self.connection.send(PolicyResult(False, str(exc)))

View File

@ -2,7 +2,12 @@
from django.core.cache import cache
from structlog.stdlib import get_logger
from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
from authentik.events.monitored_tasks import (
MonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
from authentik.policies.reputation.models import IPReputation, UserReputation
from authentik.policies.reputation.signals import CACHE_KEY_IP_PREFIX, CACHE_KEY_USER_PREFIX
from authentik.root.celery import CELERY_APP
@ -10,8 +15,9 @@ from authentik.root.celery import CELERY_APP
LOGGER = get_logger()
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
def save_ip_reputation(self: PrefilledMonitoredTask):
@CELERY_APP.task(bind=True, base=MonitoredTask)
@prefill_task
def save_ip_reputation(self: MonitoredTask):
"""Save currently cached reputation to database"""
objects_to_update = []
for key, score in cache.get_many(cache.keys(CACHE_KEY_IP_PREFIX + "*")).items():
@ -23,8 +29,9 @@ def save_ip_reputation(self: PrefilledMonitoredTask):
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated IP Reputation"]))
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
def save_user_reputation(self: PrefilledMonitoredTask):
@CELERY_APP.task(bind=True, base=MonitoredTask)
@prefill_task
def save_user_reputation(self: MonitoredTask):
"""Save currently cached reputation to database"""
objects_to_update = []
for key, score in cache.get_many(cache.keys(CACHE_KEY_USER_PREFIX + "*")).items():

View File

@ -1,31 +1,23 @@
"""OAuth2Provider API Views"""
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.fields import CharField
from rest_framework.generics import get_object_or_404
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Provider
from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider
from authentik.providers.oauth2.models import OAuth2Provider
class OAuth2ProviderSerializer(ProviderSerializer):
"""OAuth2Provider Serializer"""
def validate_jwt_alg(self, value):
"""Ensure that when RS256 is selected, a certificate-key-pair is selected"""
if self.initial_data.get("rsa_key", None) is None and value == JWTAlgorithms.RS256:
raise ValidationError(_("RS256 requires a Certificate-Key-Pair to be selected."))
return value
class Meta:
model = OAuth2Provider
@ -37,8 +29,7 @@ class OAuth2ProviderSerializer(ProviderSerializer):
"access_code_validity",
"token_validity",
"include_claims_in_id_token",
"jwt_alg",
"rsa_key",
"signing_key",
"redirect_uris",
"sub_mode",
"property_mappings",
@ -73,8 +64,7 @@ class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
"access_code_validity",
"token_validity",
"include_claims_in_id_token",
"jwt_alg",
"rsa_key",
"signing_key",
"redirect_uris",
"sub_mode",
"property_mappings",

View File

@ -0,0 +1,25 @@
# Generated by Django 4.0 on 2021-12-22 21:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
(
"authentik_providers_oauth2",
"0007_auto_20201016_1107_squashed_0017_alter_oauth2provider_token_validity",
),
]
operations = [
migrations.RenameField(
model_name="oauth2provider",
old_name="rsa_key",
new_name="signing_key",
),
migrations.RemoveField(
model_name="oauth2provider",
name="jwt_alg",
),
]

View File

@ -8,8 +8,9 @@ from datetime import datetime
from hashlib import sha256
from typing import Any, Optional, Type
from urllib.parse import urlparse
from uuid import uuid4
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from dacite import from_dict
from django.db import models
from django.http import HttpRequest
@ -89,6 +90,7 @@ class JWTAlgorithms(models.TextChoices):
HS256 = "HS256", _("HS256 (Symmetric Encryption)")
RS256 = "RS256", _("RS256 (Asymmetric Encryption)")
EC256 = "EC256", _("EC256 (Asymmetric Encryption)")
class ScopeMapping(PropertyMapping):
@ -146,13 +148,6 @@ class OAuth2Provider(Provider):
verbose_name=_("Client Secret"),
default=generate_key,
)
jwt_alg = models.CharField(
max_length=10,
choices=JWTAlgorithms.choices,
default=JWTAlgorithms.RS256,
verbose_name=_("JWT Algorithm"),
help_text=_(JWTAlgorithms.__doc__),
)
redirect_uris = models.TextField(
default="",
blank=True,
@ -208,7 +203,7 @@ class OAuth2Provider(Provider):
help_text=_(("Configure how the issuer field of the ID Token should be filled.")),
)
rsa_key = models.ForeignKey(
signing_key = models.ForeignKey(
CertificateKeyPair,
verbose_name=_("RSA Key"),
on_delete=models.SET_NULL,
@ -225,36 +220,25 @@ class OAuth2Provider(Provider):
token = RefreshToken(
user=user,
provider=self,
refresh_token=uuid4().hex,
refresh_token=generate_key(),
expires=timezone.now() + timedelta_from_string(self.token_validity),
scope=scope,
)
token.access_token = token.create_access_token(user, request)
return token
def get_jwt_key(self) -> str:
"""
Takes a provider and returns the set of keys associated with it.
Returns a list of keys.
"""
if self.jwt_alg == JWTAlgorithms.RS256:
# if the user selected RS256 but didn't select a
# CertificateKeyPair, we fall back to HS256
if not self.rsa_key:
Event.new(
EventAction.CONFIGURATION_ERROR,
provider=self,
message="Provider was configured for RS256, but no key was selected.",
).save()
self.jwt_alg = JWTAlgorithms.HS256
self.save()
else:
return self.rsa_key.key_data
if self.jwt_alg == JWTAlgorithms.HS256:
return self.client_secret
raise Exception("Unsupported key algorithm.")
def get_jwt_key(self) -> tuple[str, str]:
"""Get either the configured certificate or the client secret"""
if not self.signing_key:
# No Certificate at all, assume HS256
return self.client_secret, JWTAlgorithms.HS256
key: CertificateKeyPair = self.signing_key
private_key = key.private_key
if isinstance(private_key, RSAPrivateKey):
return key.key_data, JWTAlgorithms.RS256
if isinstance(private_key, EllipticCurvePrivateKey):
return key.key_data, JWTAlgorithms.EC256
raise Exception(f"Invalid private key type: {type(private_key)}")
def get_issuer(self, request: HttpRequest) -> Optional[str]:
"""Get issuer, based on request"""
@ -294,13 +278,13 @@ class OAuth2Provider(Provider):
def encode(self, payload: dict[str, Any]) -> str:
"""Represent the ID Token as a JSON Web Token (JWT)."""
headers = {}
if self.rsa_key:
headers["kid"] = self.rsa_key.kid
key = self.get_jwt_key()
if self.signing_key:
headers["kid"] = self.signing_key.kid
key, alg = self.get_jwt_key()
# If the provider does not have an RSA Key assigned, it was switched to Symmetric
self.refresh_from_db()
# pyright: reportGeneralTypeIssues=false
return encode(payload, key, algorithm=self.jwt_alg, headers=headers)
return encode(payload, key, algorithm=alg, headers=headers)
class Meta:
@ -434,7 +418,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
"""Create access token with a similar format as Okta, Keycloak, ADFS"""
token = self.create_id_token(user, request).to_dict()
token["cid"] = self.provider.client_id
token["uid"] = uuid4().hex
token["uid"] = generate_key()
return self.provider.encode(token)
def create_id_token(self, user: User, request: HttpRequest) -> IDToken:

View File

@ -1,32 +0,0 @@
"""Test oauth2 provider API"""
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.providers.oauth2.models import JWTAlgorithms
class TestOAuth2ProviderAPI(APITestCase):
"""Test oauth2 provider API"""
def setUp(self) -> None:
super().setUp()
self.user = create_test_admin_user()
self.client.force_login(self.user)
def test_validate(self):
"""Test OAuth2 Provider validation"""
response = self.client.post(
reverse(
"authentik_api:oauth2provider-list",
),
data={
"name": "test",
"jwt_alg": str(JWTAlgorithms.RS256),
"authorization_flow": create_test_flow().pk,
},
)
self.assertJSONEqual(
response.content.decode(),
{"jwt_alg": ["RS256 requires a Certificate-Key-Pair to be selected."]},
)

View File

@ -218,7 +218,7 @@ class TestAuthorize(OAuthTestCase):
client_secret=generate_key(),
authorization_flow=flow,
redirect_uris="http://localhost",
rsa_key=create_test_cert(),
signing_key=create_test_cert(),
)
Application.objects.create(name="app", slug="app", provider=provider)
state = generate_id()

View File

@ -25,7 +25,7 @@ class TestJWKS(OAuthTestCase):
client_id="test",
authorization_flow=create_test_flow(),
redirect_uris="http://local.invalid",
rsa_key=create_test_cert(),
signing_key=create_test_cert(),
)
app = Application.objects.create(name="test", slug="test", provider=provider)
response = self.client.get(

View File

@ -35,7 +35,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://testserver",
rsa_key=create_test_cert(),
signing_key=create_test_cert(),
)
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
user = create_test_admin_user()
@ -62,7 +62,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://testserver",
rsa_key=create_test_cert(),
signing_key=create_test_cert(),
)
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
request = self.factory.post(
@ -85,7 +85,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://local.invalid",
rsa_key=create_test_cert(),
signing_key=create_test_cert(),
)
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
user = create_test_admin_user()
@ -114,7 +114,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://local.invalid",
rsa_key=create_test_cert(),
signing_key=create_test_cert(),
)
# Needs to be assigned to an application for iss to be set
self.app.provider = provider
@ -156,7 +156,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://local.invalid",
rsa_key=create_test_cert(),
signing_key=create_test_cert(),
)
# Needs to be assigned to an application for iss to be set
self.app.provider = provider
@ -205,7 +205,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://local.invalid",
rsa_key=create_test_cert(),
signing_key=create_test_cert(),
)
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
user = create_test_admin_user()
@ -250,7 +250,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://testserver",
rsa_key=create_test_cert(),
signing_key=create_test_cert(),
)
# Needs to be assigned to an application for iss to be set
self.app.provider = provider

View File

@ -27,7 +27,7 @@ class TestUserinfo(OAuthTestCase):
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="",
rsa_key=create_test_cert(),
signing_key=create_test_cert(),
)
self.provider.property_mappings.set(ScopeMapping.objects.all())
# Needs to be assigned to an application for iss to be set

View File

@ -19,13 +19,13 @@ class OAuthTestCase(TestCase):
def validate_jwt(self, token: RefreshToken, provider: OAuth2Provider):
"""Validate that all required fields are set"""
key = provider.client_secret
if provider.jwt_alg == JWTAlgorithms.RS256:
key = provider.rsa_key.public_key
key, alg = provider.get_jwt_key()
if alg != JWTAlgorithms.HS256:
key = provider.signing_key.public_key
jwt = decode(
token.access_token,
key,
algorithms=[provider.jwt_alg],
algorithms=[alg],
audience=provider.client_id,
)
id_token = token.id_token.to_dict()

View File

@ -1,12 +1,17 @@
"""authentik OAuth2 JWKS Views"""
from base64 import urlsafe_b64encode
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from cryptography.hazmat.primitives.asymmetric.ec import (
EllipticCurvePrivateKey,
EllipticCurvePublicKey,
)
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from django.views import View
from authentik.core.models import Application
from authentik.crypto.models import CertificateKeyPair
from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider
@ -25,22 +30,39 @@ class JWKSView(View):
"""Show RSA Key data for Provider"""
application = get_object_or_404(Application, slug=application_slug)
provider: OAuth2Provider = get_object_or_404(OAuth2Provider, pk=application.provider_id)
signing_key: CertificateKeyPair = provider.signing_key
response_data = {}
if provider.jwt_alg == JWTAlgorithms.RS256 and provider.rsa_key:
public_key: RSAPublicKey = provider.rsa_key.private_key.public_key()
public_numbers = public_key.public_numbers()
response_data["keys"] = [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": provider.rsa_key.kid,
"n": b64_enc(public_numbers.n),
"e": b64_enc(public_numbers.e),
}
]
if signing_key:
private_key = signing_key.private_key
print(type(private_key))
if isinstance(private_key, RSAPrivateKey):
public_key: RSAPublicKey = private_key.public_key()
public_numbers = public_key.public_numbers()
response_data["keys"] = [
{
"kty": "RSA",
"alg": JWTAlgorithms.RS256,
"use": "sig",
"kid": signing_key.kid,
"n": b64_enc(public_numbers.n),
"e": b64_enc(public_numbers.e),
}
]
elif isinstance(private_key, EllipticCurvePrivateKey):
public_key: EllipticCurvePublicKey = private_key.public_key()
public_numbers = public_key.public_numbers()
response_data["keys"] = [
{
"kty": "EC",
"alg": JWTAlgorithms.EC256,
"use": "sig",
"kid": signing_key.kid,
"n": b64_enc(public_numbers.n),
"e": b64_enc(public_numbers.e),
}
]
response = JsonResponse(response_data)
response["Access-Control-Allow-Origin"] = "*"

View File

@ -39,6 +39,7 @@ class ProviderInfoView(View):
)
if SCOPE_OPENID not in scopes:
scopes.append(SCOPE_OPENID)
_, supported_alg = provider.get_jwt_key()
return {
"issuer": provider.get_issuer(self.request),
"authorization_endpoint": self.request.build_absolute_uri(
@ -78,7 +79,7 @@ class ProviderInfoView(View):
GRANT_TYPE_REFRESH_TOKEN,
GrantTypes.IMPLICIT,
],
"id_token_signing_alg_values_supported": [provider.jwt_alg],
"id_token_signing_alg_values_supported": [supported_alg],
# See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
"subject_types_supported": ["public"],
"token_endpoint_auth_methods_supported": [

View File

@ -95,9 +95,15 @@ class TokenParams:
self.refresh_token = RefreshToken.objects.get(
refresh_token=raw_token, provider=self.provider
)
if self.refresh_token.is_expired:
LOGGER.warning(
"Refresh token is expired",
token=raw_token,
)
raise TokenError("invalid_grant")
# https://tools.ietf.org/html/rfc6749#section-6
# Fallback to original token's scopes when none are given
if self.scope == []:
if not self.scope:
self.scope = self.refresh_token.scope
except RefreshToken.DoesNotExist:
LOGGER.warning(
@ -138,6 +144,12 @@ class TokenParams:
try:
self.authorization_code = AuthorizationCode.objects.get(code=raw_code)
if self.authorization_code.is_expired:
LOGGER.warning(
"Code is expired",
token=raw_code,
)
raise TokenError("invalid_grant")
except AuthorizationCode.DoesNotExist:
LOGGER.warning("Code does not exist", code=raw_code)
raise TokenError("invalid_grant")
@ -194,8 +206,10 @@ class TokenView(View):
self.params = TokenParams.parse(request, self.provider, client_id, client_secret)
if self.params.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
LOGGER.info("Converting authorization code to refresh token")
return TokenResponse(self.create_code_response())
if self.params.grant_type == GRANT_TYPE_REFRESH_TOKEN:
LOGGER.info("Refreshing refresh token")
return TokenResponse(self.create_refresh_response())
raise ValueError(f"Invalid grant_type: {self.params.grant_type}")
except TokenError as error:

View File

@ -89,6 +89,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
# goes to the same pod
"nginx.ingress.kubernetes.io/affinity": "cookie",
"traefik.ingress.kubernetes.io/affinity": "true",
# Buffer sizes for large headers with JWTs
"nginx.ingress.kubernetes.io/proxy-buffers-number": "4",
"nginx.ingress.kubernetes.io/proxy-buffer-size": "16k",
}

View File

@ -96,6 +96,16 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
super().reconcile(current, reference)
if current.spec.forwardAuth.address != reference.spec.forwardAuth.address:
raise NeedsUpdate()
if (
current.spec.forwardAuth.authResponseHeadersRegex
!= reference.spec.forwardAuth.authResponseHeadersRegex
):
raise NeedsUpdate()
# Ensure all of our headers are set, others can be added by the user.
if not set(current.spec.forwardAuth.authResponseHeaders).issubset(
reference.spec.forwardAuth.authResponseHeaders
):
raise NeedsUpdate()
def get_reference_object(self) -> TraefikMiddleware:
"""Get deployment object for outpost"""
@ -110,8 +120,27 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
spec=TraefikMiddlewareSpec(
forwardAuth=TraefikMiddlewareSpecForwardAuth(
address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik",
authResponseHeaders=[],
authResponseHeadersRegex="^.*$",
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",
"X-authentik-name",
"X-authentik-uid",
"X-authentik-jwt",
"X-authentik-meta-jwks",
"X-authentik-meta-outpost",
"X-authentik-meta-provider",
"X-authentik-meta-app",
"X-authentik-meta-version",
],
authResponseHeadersRegex="",
trustForwardHeader=True,
)
),

View File

@ -2,6 +2,7 @@
import django.db.models.deletion
from django.apps.registry import Apps
from django.core.exceptions import FieldError
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
@ -10,12 +11,17 @@ import authentik.providers.proxy.models
def migrate_defaults(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.providers.proxy.models import JWTAlgorithms, ProxyProvider
from authentik.providers.oauth2.models import JWTAlgorithms
from authentik.providers.proxy.models import ProxyProvider
db_alias = schema_editor.connection.alias
for provider in ProxyProvider.objects.using(db_alias).filter(jwt_alg=JWTAlgorithms.RS256):
provider.set_oauth_defaults()
provider.save()
try:
for provider in ProxyProvider.objects.using(db_alias).filter(jwt_alg=JWTAlgorithms.RS256):
provider.set_oauth_defaults()
provider.save()
except FieldError:
# If the jwt_alg field doesn't exist, just ignore this migration
pass
def migrate_mode(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):

View File

@ -1,17 +1,23 @@
# Generated by Django 3.2.6 on 2021-09-09 11:24
from django.apps.registry import Apps
from django.core.exceptions import FieldError
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_defaults(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.providers.proxy.models import JWTAlgorithms, ProxyProvider
from authentik.providers.oauth2.models import JWTAlgorithms
from authentik.providers.proxy.models import ProxyProvider
db_alias = schema_editor.connection.alias
for provider in ProxyProvider.objects.using(db_alias).filter(jwt_alg=JWTAlgorithms.RS256):
provider.set_oauth_defaults()
provider.save()
try:
for provider in ProxyProvider.objects.using(db_alias).filter(jwt_alg=JWTAlgorithms.RS256):
provider.set_oauth_defaults()
provider.save()
except FieldError:
# If the jwt_alg field doesn't exist, just ignore this migration
pass
class Migration(migrations.Migration):

View File

@ -16,12 +16,7 @@ from authentik.providers.oauth2.constants import (
SCOPE_OPENID_EMAIL,
SCOPE_OPENID_PROFILE,
)
from authentik.providers.oauth2.models import (
ClientTypes,
JWTAlgorithms,
OAuth2Provider,
ScopeMapping,
)
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
SCOPE_AK_PROXY = "ak_proxy"
@ -128,8 +123,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
def set_oauth_defaults(self):
"""Ensure all OAuth2-related settings are correct"""
self.client_type = ClientTypes.CONFIDENTIAL
self.jwt_alg = JWTAlgorithms.HS256
self.rsa_key = None
self.signing_key = None
scopes = ScopeMapping.objects.filter(
scope_name__in=[
SCOPE_OPENID,

View File

@ -36,6 +36,7 @@ from authentik.flows.models import Flow, FlowDesignation
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.metadata import MetadataProcessor
from authentik.providers.saml.processors.metadata_parser import ServiceProviderMetadataParser
from authentik.sources.saml.processors.constants import SAML_BINDING_POST, SAML_BINDING_REDIRECT
LOGGER = get_logger()
@ -109,7 +110,17 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
name="download",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
)
),
OpenApiParameter(
name="force_binding",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR,
enum=[
SAML_BINDING_REDIRECT,
SAML_BINDING_POST,
],
description=("Optionally force the metadata to only include one binding."),
),
],
)
@action(methods=["GET"], detail=True, permission_classes=[AllowAny])
@ -122,8 +133,10 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
except ValueError:
raise Http404
try:
metadata = MetadataProcessor(provider, request).build_entity_descriptor()
if "download" in request._request.GET:
proc = MetadataProcessor(provider, request)
proc.force_binding = request.query_params.get("force_binding", None)
metadata = proc.build_entity_descriptor()
if "download" in request.query_params:
response = HttpResponse(metadata, content_type="application/xml")
response[
"Content-Disposition"

View File

@ -21,7 +21,7 @@ class Migration(migrations.Migration):
name="audience",
field=models.TextField(
default="",
help_text="Value of the audience restriction field of the asseration.",
help_text="Value of the audience restriction field of the assertion.",
),
),
migrations.AlterField(

View File

@ -16,7 +16,7 @@ class Migration(migrations.Migration):
field=models.TextField(
blank=True,
default="",
help_text="Value of the audience restriction field of the asseration. When left empty, no audience restriction will be added.",
help_text="Value of the audience restriction field of the assertion. When left empty, no audience restriction will be added.",
),
),
]

View File

@ -41,7 +41,7 @@ class SAMLProvider(Provider):
blank=True,
help_text=_(
(
"Value of the audience restriction field of the asseration. When left empty, "
"Value of the audience restriction field of the assertion. When left empty, "
"no audience restriction will be added."
)
),

View File

@ -70,13 +70,14 @@ class AssertionProcessor:
"""Get AttributeStatement Element with Attributes from Property Mappings."""
# https://commons.lbl.gov/display/IDMgmt/Attribute+Definitions
attribute_statement = Element(f"{{{NS_SAML_ASSERTION}}}AttributeStatement")
user = self.http_request.user
for mapping in self.provider.property_mappings.all().select_subclasses():
if not isinstance(mapping, SAMLPropertyMapping):
continue
try:
mapping: SAMLPropertyMapping
value = mapping.evaluate(
user=self.http_request.user,
user=user,
request=self.http_request,
provider=self.provider,
)
@ -101,7 +102,8 @@ class AssertionProcessor:
attribute_statement.append(attribute)
except PropertyMappingExpressionException as exc:
except (PropertyMappingExpressionException, ValueError) as exc:
# Value error can be raised when assigning invalid data to an attribute
Event.new(
EventAction.CONFIGURATION_ERROR,
message=f"Failed to evaluate property-mapping: {str(exc)}",

View File

@ -29,10 +29,12 @@ class MetadataProcessor:
provider: SAMLProvider
http_request: HttpRequest
force_binding: Optional[str]
def __init__(self, provider: SAMLProvider, request: HttpRequest):
self.provider = provider
self.http_request = request
self.force_binding = None
self.xml_id = get_random_id()
def get_signing_key_descriptor(self) -> Optional[Element]:
@ -79,6 +81,8 @@ class MetadataProcessor:
),
}
for binding, url in binding_url_map.items():
if self.force_binding and self.force_binding != binding:
continue
element = Element(f"{{{NS_SAML_METADATA}}}SingleSignOnService")
element.attrib["Binding"] = binding
element.attrib["Location"] = url

View File

@ -125,7 +125,7 @@ class SAMLSSOBindingPOSTView(SAMLSSOView):
# This happens when using POST bindings but the user isn't logged in
# (user gets redirected and POST body is 'lost')
if SESSION_KEY_POST in self.request.session:
payload = self.request.session[SESSION_KEY_POST]
payload = self.request.session.pop(SESSION_KEY_POST)
if REQUEST_KEY_SAML_REQUEST not in payload:
LOGGER.info("check_saml_request: SAML payload missing")
return bad_request_message(self.request, "The SAML request payload is missing.")

View File

@ -14,6 +14,7 @@ from celery.signals import (
from django.conf import settings
from structlog.stdlib import get_logger
from authentik.core.middleware import LOCAL
from authentik.lib.sentry import before_send
from authentik.lib.utils.errors import exception_to_string
@ -26,7 +27,7 @@ CELERY_APP = Celery("authentik")
# pylint: disable=unused-argument
@setup_logging.connect
def config_loggers(*args, **kwags):
def config_loggers(*args, **kwargs):
"""Apply logging settings from settings.py to celery"""
dictConfig(settings.LOGGING)
@ -36,21 +37,29 @@ def config_loggers(*args, **kwags):
def after_task_publish_hook(sender=None, headers=None, body=None, **kwargs):
"""Log task_id after it was published"""
info = headers if "task" in headers else body
LOGGER.debug("Task published", task_id=info.get("id", ""), task_name=info.get("task", ""))
LOGGER.info("Task published", task_id=info.get("id", ""), task_name=info.get("task", ""))
# pylint: disable=unused-argument
@task_prerun.connect
def task_prerun_hook(task_id, task, *args, **kwargs):
def task_prerun_hook(task_id: str, task, *args, **kwargs):
"""Log task_id on worker"""
LOGGER.debug("Task started", task_id=task_id, task_name=task.__name__)
request_id = "task-" + task_id.replace("-", "")
LOCAL.authentik_task = {
"request_id": request_id,
}
LOGGER.info("Task started", task_id=task_id, task_name=task.__name__)
# pylint: disable=unused-argument
@task_postrun.connect
def task_postrun_hook(task_id, task, *args, retval=None, state=None, **kwargs):
"""Log task_id on worker"""
LOGGER.debug("Task finished", task_id=task_id, task_name=task.__name__, state=state)
LOGGER.info("Task finished", task_id=task_id, task_name=task.__name__, state=state)
if not hasattr(LOCAL, "authentik_task"):
return
for key in list(LOCAL.authentik_task.keys()):
del LOCAL.authentik_task[key]
# pylint: disable=unused-argument

View File

@ -24,9 +24,11 @@ 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.core.middleware import structlog_add_request_id
@ -65,7 +67,7 @@ SECRET_KEY = CONFIG.y("secret_key")
INTERNAL_IPS = ["127.0.0.1"]
ALLOWED_HOSTS = ["*"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_CROSS_ORIGIN_OPENER_POLICY = None
LOGIN_URL = "authentik_flows:default-authentication"
# Custom user model
@ -218,19 +220,21 @@ REDIS_CELERY_TLS_REQUIREMENTS = ""
if CONFIG.y_bool("redis.tls", False):
REDIS_PROTOCOL_PREFIX = "rediss://"
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"{int(CONFIG.y('redis.port'))}"
)
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": (
f"{REDIS_PROTOCOL_PREFIX}:"
f"{quote(CONFIG.y('redis.password'))}@{quote(CONFIG.y('redis.host'))}:"
f"{int(CONFIG.y('redis.port'))}/{CONFIG.y('redis.cache_db')}"
),
"LOCATION": f"{_redis_url}/{CONFIG.y('redis.cache_db')}",
"TIMEOUT": int(CONFIG.y("redis.cache_timeout", 300)),
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
}
}
DJANGO_REDIS_SCAN_ITERSIZE = 1000
DJANGO_REDIS_IGNORE_EXCEPTIONS = True
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
@ -284,11 +288,7 @@ CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [
f"{REDIS_PROTOCOL_PREFIX}:"
f"{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:"
f"{int(CONFIG.y('redis.port'))}/{CONFIG.y('redis.ws_db')}"
],
"hosts": [f"{_redis_url}/{CONFIG.y('redis.ws_db')}"],
},
},
}
@ -342,8 +342,6 @@ TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True
LOCALE_PATHS = ["./locale"]
@ -366,16 +364,10 @@ CELERY_BEAT_SCHEDULE = {
CELERY_TASK_CREATE_MISSING_QUEUES = True
CELERY_TASK_DEFAULT_QUEUE = "authentik"
CELERY_BROKER_URL = (
f"{REDIS_PROTOCOL_PREFIX}:"
f"{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:"
f"{int(CONFIG.y('redis.port'))}/{CONFIG.y('redis.message_queue_db')}"
f"{REDIS_CELERY_TLS_REQUIREMENTS}"
f"{_redis_url}/{CONFIG.y('redis.message_queue_db')}{REDIS_CELERY_TLS_REQUIREMENTS}"
)
CELERY_RESULT_BACKEND = (
f"{REDIS_PROTOCOL_PREFIX}:"
f"{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:"
f"{int(CONFIG.y('redis.port'))}/{CONFIG.y('redis.message_queue_db')}"
f"{REDIS_CELERY_TLS_REQUIREMENTS}"
f"{_redis_url}/{CONFIG.y('redis.message_queue_db')}{REDIS_CELERY_TLS_REQUIREMENTS}"
)
# Database backup
@ -421,6 +413,8 @@ if _ERROR_REPORTING:
DjangoIntegration(transaction_style="function_name"),
CeleryIntegration(),
RedisIntegration(),
Boto3Integration(),
ThreadingIntegration(propagate_hub=True),
],
before_send=before_send,
release=f"authentik@{__version__}",
@ -467,6 +461,11 @@ TEST = False
TEST_RUNNER = "authentik.root.test_runner.PytestTestRunner"
# We can't check TEST here as its set later by the test runner
LOG_LEVEL = CONFIG.y("log_level").upper() if "TF_BUILD" not in os.environ else "DEBUG"
# We could add a custom level to stdlib logging and structlog, but it's not easy or clean
# https://stackoverflow.com/questions/54505487/custom-log-level-not-working-with-structlog
# Additionally, the entire code uses debug as highest level so that would have to be re-written too
if LOG_LEVEL == "TRACE":
LOG_LEVEL = "DEBUG"
structlog.configure_once(
processors=[

View File

@ -1,4 +1,6 @@
"""Integrate ./manage.py test with pytest"""
from argparse import ArgumentParser
from django.conf import settings
from authentik.lib.config import CONFIG
@ -8,34 +10,43 @@ from tests.e2e.utils import get_docker_tag
class PytestTestRunner: # pragma: no cover
"""Runs pytest to discover and run tests."""
def __init__(self, verbosity=1, failfast=False, keepdb=False, **_):
def __init__(self, verbosity=1, failfast=False, keepdb=False, **kwargs):
self.verbosity = verbosity
self.failfast = failfast
self.keepdb = keepdb
self.args = ["-vv"]
if self.failfast:
self.args.append("--exitfirst")
if self.keepdb:
self.args.append("--reuse-db")
if kwargs.get("randomly_seed", None):
self.args.append(f"--randomly-seed={kwargs['randomly_seed']}")
settings.TEST = True
settings.CELERY_TASK_ALWAYS_EAGER = True
CONFIG.y_set("authentik.avatars", "none")
CONFIG.y_set("authentik.geoip", "tests/GeoLite2-City-Test.mmdb")
CONFIG.y_set(
"outposts.container_image_base",
f"goauthentik.io/dev-%(type)s:{get_docker_tag()}",
f"ghcr.io/goauthentik/dev-%(type)s:{get_docker_tag()}",
)
@classmethod
def add_arguments(cls, parser: ArgumentParser):
"""Add more pytest-specific arguments"""
parser.add_argument("--randomly-seed", type=int)
def run_tests(self, test_labels):
"""Run pytest and return the exitcode.
It translates some of Django's test command option to pytest's.
"""
import pytest
argv = ["-vv"]
if self.failfast:
argv.append("--exitfirst")
if self.keepdb:
argv.append("--reuse-db")
if any("tests/e2e" in label for label in test_labels):
argv.append("-pno:randomly")
argv.extend(test_labels)
return pytest.main(argv)
self.args.append("-pno:randomly")
self.args.extend(test_labels)
return pytest.main(self.args)

View File

@ -1,7 +1,6 @@
"""Source API Views"""
from typing import Any
from django.utils.text import slugify
from django_filters.filters import AllValuesMultipleFilter
from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
@ -110,7 +109,8 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
GroupLDAPSynchronizer,
MembershipLDAPSynchronizer,
]:
task = TaskInfo.by_name(f"ldap_sync_{slugify(source.name)}-{sync_class.__name__}")
sync_name = sync_class.__name__.replace("LDAPSynchronizer", "").lower()
task = TaskInfo.by_name(f"ldap_sync_{source.slug}_{sync_name}")
if task:
results.append(task)
return Response(TaskSerializer(results, many=True).data)

View File

@ -29,7 +29,7 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
group_dn = self._flatten(self._flatten(group.get("entryDN", group.get("dn"))))
if self._source.object_uniqueness_field not in attributes:
self.message(
f"Cannot find uniqueness field in attributes: '{group_dn}",
f"Cannot find uniqueness field in attributes: '{group_dn}'",
attributes=attributes.keys(),
dn=group_dn,
)

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