Compare commits

..

418 Commits

Author SHA1 Message Date
e25f6aea8c release: 2021.6.1-rc4 2021-06-10 18:59:00 +02:00
b1a9eda1d3 ci: fix release test using wrong docker image
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 18:58:30 +02:00
2c15ab9995 release: 2021.6.1-rc3 2021-06-10 18:04:59 +02:00
b3c51e426d web: fix styling for toggle group on dark mode
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 18:02:27 +02:00
71578af47f ci: fix testing for release tag
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 17:41:54 +02:00
6c985acb36 release: 2021.6.1-rc2 2021-06-10 14:10:47 +02:00
d878d2140e providers/saml: add metadata download link to api
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 14:06:44 +02:00
4766d6ff3d flows: add export URL to API
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 13:52:50 +02:00
3a64d97040 crypto: add download links as API fields
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 13:46:12 +02:00
2275ba3add flows: fix get_pending_user returning in-memory user when PLAN_CONTEXT_PENDING_USER_IDENTIFIER is set
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 12:17:46 +02:00
9f7c941426 Merge branch 'master' into next 2021-06-10 11:59:10 +02:00
34ae9e6dab API: add endpoint to show by what objects an object is used (#995)
* core: add used_by API to show what objects are affected before deletion

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

* web/elements: add support for used_by API

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

* core: add authentik_used_by_shadows to shadow other models

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

* web: implement used_by API

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

* *: fix duplicate imports

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

* core: add action field to used_by api

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

* web: add UI for used_by action

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

* web: add notice to tenant form

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

* core: fix naming in used_by

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

* web: check length for used_by

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

* core: fix used_by for non-pk models

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

* *: improve __str__ on models

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

* core: add support for many to many in used_by

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 11:58:12 +02:00
bf683514ee build(deps): bump @babel/plugin-proposal-decorators in /web (#1000) 2021-06-10 09:11:01 +02:00
9b58bdb447 build(deps): bump @babel/preset-env from 7.14.4 to 7.14.5 in /web (#1002) 2021-06-10 09:10:52 +02:00
4237f20ccd build(deps): bump boto3 from 1.17.90 to 1.17.91 (#1003) 2021-06-10 08:53:42 +02:00
2408719a47 build(deps): bump eslint-plugin-lit from 1.5.0 to 1.5.1 in /web (#1001) 2021-06-10 08:53:35 +02:00
b33fef7929 build(deps): bump @babel/preset-typescript from 7.13.0 to 7.14.5 in /web (#999) 2021-06-10 08:53:20 +02:00
73b9847e7d build(deps): bump @babel/core from 7.14.3 to 7.14.5 in /web (#998) 2021-06-10 08:53:10 +02:00
a7e4eb021d build(deps): bump @babel/plugin-transform-runtime in /web (#997) 2021-06-10 08:53:01 +02:00
11306770ad build(deps): bump postcss from 8.3.0 to 8.3.1 in /website (#996) 2021-06-10 08:52:51 +02:00
5235e00d3c stages/authenticator_validate: add more logging for challenges
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 23:58:08 +02:00
7834146efc web/admin: fix authenticatior_valiation stage not setting correct fields
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 19:38:54 +02:00
d4379ecd31 flows: fix configure_url not being set correctly User settings
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 19:25:27 +02:00
7492608ace Merge branch 'version-2021.6' into next 2021-06-09 16:06:06 +02:00
7eef501446 Revert "root: fix permissions for docker files"
This reverts commit a7adeb917e.
2021-06-09 16:04:17 +02:00
b73de96aa6 lifecycle: fix permissions for unittest xml
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 16:03:51 +02:00
a7adeb917e root: fix permissions for docker files
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 16:00:29 +02:00
4ee2f951da lifecycle: fix check_if_root not working without docker
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:56:12 +02:00
01c5235e82 ci: use bootstrap for testing
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:54:47 +02:00
0ce4f9fe12 Revert "web: don't build api client as separate bundle"
This reverts commit 7c1fe1243f.
2021-06-09 15:37:57 +02:00
2f4f951818 Revert "web: build API during npm build"
This reverts commit a6c214e8fa.
2021-06-09 15:37:50 +02:00
a6c214e8fa web: build API during npm build
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:35:35 +02:00
57f8b108c4 root: remove production=false
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:27:06 +02:00
7c1fe1243f web: don't build api client as separate bundle
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:26:42 +02:00
3f69dd34ba ci: run tests as authentik
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:05:03 +02:00
c81431895a Merge branch 'master' into version-2021.6 2021-06-09 15:04:52 +02:00
560c979d26 root: fix requirements-dev including all dependencies
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 14:22:45 +02:00
c5cc8842ec root: fix missing test files in docker file
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 14:22:32 +02:00
2a881d241d Merge branch 'master' into next 2021-06-09 11:25:07 +02:00
6291834573 outpost: fix missing outpost images
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 11:24:59 +02:00
eeea36acea outpost: fix missing outpost images
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 11:22:28 +02:00
e95b9da586 website/docs: fix beta instructions for k8s
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 11:07:02 +02:00
f4a53c89ef release: 2021.6.1-rc1 2021-06-09 11:01:14 +02:00
20493252e2 lifecycle: fix custom port not being set for postgres healthcheck
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 10:59:48 +02:00
2210497569 events: add EMAIL_SENT event, show sent emails in event log
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 10:28:32 +02:00
2addf71f37 outposts: add service connection to outpost API
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 09:45:14 +02:00
de11181890 web/admin: fix outpost/ldap charts when no healthy objects exist
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 09:31:30 +02:00
66e3bc6b58 build(deps): bump @lingui/core from 3.9.0 to 3.10.2 in /web (#989)
Bumps [@lingui/core](https://github.com/lingui/js-lingui) from 3.9.0 to 3.10.2.
- [Release notes](https://github.com/lingui/js-lingui/releases)
- [Changelog](https://github.com/lingui/js-lingui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lingui/js-lingui/compare/v3.9.0...v3.10.2)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-09 09:24:05 +02:00
612679e8df build(deps): bump @lingui/macro from 3.9.0 to 3.10.2 in /web (#992)
Bumps [@lingui/macro](https://github.com/lingui/js-lingui) from 3.9.0 to 3.10.2.
- [Release notes](https://github.com/lingui/js-lingui/releases)
- [Changelog](https://github.com/lingui/js-lingui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lingui/js-lingui/compare/v3.9.0...v3.10.2)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-09 09:19:30 +02:00
c9072f7403 build(deps): bump rollup from 2.51.0 to 2.51.1 in /web (#988) 2021-06-09 08:31:42 +02:00
cacacb06af build(deps): bump @patternfly/patternfly from 4.103.6 to 4.108.2 in /web (#990) 2021-06-09 08:31:23 +02:00
7da87a53b7 build(deps): bump @lingui/cli from 3.9.0 to 3.10.2 in /web (#991) 2021-06-09 08:29:41 +02:00
9f894881ca build(deps): bump boto3 from 1.17.89 to 1.17.90 (#993) 2021-06-09 08:29:31 +02:00
dad24c03ff outposts: set cookies for a domain to authenticate an entire domain (#971)
* outposts: initial cookie domain implementation

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

* web/admin: add cookie domain setting

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

* providers/proxy: replace forward_auth_mode with general mode

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

* web/admin: rebuild proxy provider form

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

* providers/proxy: re-add forward_auth_mode for backwards compat

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

* web/admin: fix data.mode not being set

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

* root: always set log level to debug when testing

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

* providers/proxy: use new mode attribute

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

* providers/proxy: only ingress /akprox on forward_domain

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

* providers/proxy: fix lint error

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

* web/admin: fix error on ProxyProviderForm when not using proxy mode

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

* web/admin: fix default for outpost form's type missing

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

* web/admin: add additional desc for proxy modes

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

* outposts: fix service account permissions not always being updated

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

* outpost/proxy: fix redirecting to incorrect host for domain mode

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

* web: improve error handling for network errors

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

* outpost: fix image naming not matching main imaeg

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

* outposts/proxy: fix redirects for domain mode and traefik

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

* web: fix colour for paragraphs

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

* web/flows: fix consent stage not showing permissions correctly

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

* website/docs: add domain-level docs

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

* website/docs: fix broken links

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

* outposts/proxy: remove dead code

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

* web/flows: fix missing id for #header-text

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-08 23:10:17 +02:00
fb8d67a9d9 core: add configure_url to UserSettings for both stages and sources
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-08 19:21:27 +02:00
029d58191e sources/saml: include metadata download link in API response
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-08 17:22:03 +02:00
75404f1345 web/admin: pass full configure flow URL instead of just boolean
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-08 17:14:54 +02:00
ba1b23c879 flows: move flow relevant info into ContextualFlowInfo
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-08 16:53:28 +02:00
ae8cf00a21 tests/e2e: switch to ghcr images
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-08 15:32:23 +02:00
d9ffb23a80 web/admin: improve ldap form
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-08 09:31:36 +02:00
dab5f4c768 build(deps): bump @typescript-eslint/parser in /web (#982)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.26.0 to 4.26.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.26.1/packages/parser)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-08 09:27:20 +02:00
cd6632fca6 build(deps): bump @typescript-eslint/eslint-plugin in /web (#984) 2021-06-08 08:54:42 +02:00
ea1741838c build(deps): bump eslint-plugin-lit from 1.4.1 to 1.5.0 in /web (#983) 2021-06-08 08:54:33 +02:00
8256fa8c0b build(deps): bump duo-client from 4.3.1 to 4.3.2 (#985) 2021-06-08 08:54:03 +02:00
486a930163 build(deps): bump boto3 from 1.17.88 to 1.17.89 (#986) 2021-06-08 08:53:43 +02:00
8a58a31bd6 build(deps): bump github.com/go-openapi/runtime in /outpost (#987) 2021-06-08 08:53:34 +02:00
deb0d3f7bc website: add LDAP to protocols
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-07 22:27:53 +02:00
10208b45b6 website/docs: fix API browser not loading
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-07 22:19:11 +02:00
25f987ba2b stages/prompt: add more tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-07 17:40:06 +02:00
f23111beff stages/user_write: add tests for duplicate data
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-07 16:42:01 +02:00
0f693158b6 stages/email: add tests for inaccessible email templates
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-07 16:09:39 +02:00
e51226432f web: update default flow background
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-07 10:18:40 +02:00
b1fbcef98a build(deps-dev): bump prettier from 2.3.0 to 2.3.1 in /website (#979) 2021-06-07 08:38:07 +02:00
ce56192412 build(deps-dev): bump pytest-django from 4.3.0 to 4.4.0 (#980) 2021-06-07 08:37:56 +02:00
70d72f340f outpost: fix default value for buildHash
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 23:07:23 +02:00
7524e114d9 outpost/ldap: add http server for healthchecks
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 23:07:13 +02:00
4d7dab92bc website/docs: add changelog for 2021.6
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 19:43:47 +02:00
a36e3aa3a4 web: rename Form.reset to resetForm to prevent t.form is not a function error
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 19:28:16 +02:00
fceab788d2 outposts: fix error during outpost disconnect
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 19:25:09 +02:00
d55d44d664 web/user: fix deletion of authenticator not refreshing page
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 19:09:29 +02:00
88cc38394e root: improve sentry tags to simplify queries
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 14:51:43 +02:00
ea1696a275 root: add stale.yml
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 14:47:03 +02:00
552d26eb98 outpost: only build in docker
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 14:11:38 +02:00
90a5c84ac8 core: make EndSessionView inherit PolicyAccessView
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 14:07:50 +02:00
b55c3a687d web/admin: fix default for policy binding negate
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 14:04:20 +02:00
e786244988 root: don't run main ci for website or outpost changes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 13:56:38 +02:00
68f1fbebf4 root: add question template
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 13:56:38 +02:00
9180d448df core: move end-session to core
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 13:56:38 +02:00
67470590c2 web/flows: only show permissions header when permissions are defined
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 13:56:38 +02:00
fe2e850303 build(deps): bump rollup from 2.50.6 to 2.51.0 in /web (#977)
Bumps [rollup](https://github.com/rollup/rollup) from 2.50.6 to 2.51.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.50.6...v2.51.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-06 13:56:24 +02:00
a7a3c158ea build(deps): bump eslint from 7.27.0 to 7.28.0 in /web (#976)
Bumps [eslint](https://github.com/eslint/eslint) from 7.27.0 to 7.28.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v7.27.0...v7.28.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-06 13:56:13 +02:00
98d0986ac8 build(deps): bump boto3 from 1.17.87 to 1.17.88 (#978)
Bumps [boto3](https://github.com/boto/boto3) from 1.17.87 to 1.17.88.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.17.87...1.17.88)

---
updated-dependencies:
- dependency-name: boto3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-06 13:56:05 +02:00
bedf7fbcaa web/admin: don't show backup error when server can't backup
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 12:51:23 +02:00
1f35f73c66 api: add CAN_BACKUP capability
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 12:44:43 +02:00
8ea02e4cc9 web/admin: fix charts not showing with null values
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 12:40:32 +02:00
f399b32135 web: fix form for charts not matching the rest
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 12:40:20 +02:00
0032f535da core: add minor tests for users api
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 01:23:04 +02:00
3c349b1f22 tests: fix missing test GeoIP database
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 01:08:11 +02:00
17326615b7 events: rewrite GeoIP to a wrapper, reload file every 8 hours
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-06 00:42:41 +02:00
f5dbdbd48b *: add clear param to file upload API to delete stored file and reset field
closes #949

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-05 21:33:03 +02:00
277c2f4aad core: make application.meta_icon nullable
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#949
2021-06-05 21:06:52 +02:00
d38f944435 web: fix ModalForm loading data even when not in viewport
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-05 20:51:58 +02:00
ba3e0a0586 core: fix flow query
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-05 20:30:56 +02:00
7581c84a37 flows: fix tests using flow.background.url
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-05 20:29:31 +02:00
86b450c6d1 flows: add compatibility_mode to toggle ShadyDOM
closes #894

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-05 20:04:30 +02:00
e43e42139a web: migrate templates back to django
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-05 19:38:24 +02:00
0b90cfcec4 flows: set default background in code not model
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-05 19:38:13 +02:00
cefe3fa6dd outposts: fix docker controller always replacing beta images
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-05 16:08:20 +02:00
24da24b5d5 stages/identification: allow setting of a password stage to check password and identity in a single step
closes #970

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-05 16:05:03 +02:00
f996f9d4e3 tests/e2e: ensure outpost service account has correct permissions
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-04 09:46:31 +02:00
5411412626 build(deps): bump golang from 1.16.4 to 1.16.5 in /outpost (#966)
Bumps golang from 1.16.4 to 1.16.5.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-04 09:37:13 +02:00
f9050f9192 build(deps): bump golang from 1.16.4 to 1.16.5 (#967)
Bumps golang from 1.16.4 to 1.16.5.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-04 09:37:02 +02:00
bc75c07e65 build(deps): bump rollup from 2.50.5 to 2.50.6 in /web (#968)
Bumps [rollup](https://github.com/rollup/rollup) from 2.50.5 to 2.50.6.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.50.5...v2.50.6)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-04 09:35:59 +02:00
c02b943612 build(deps): bump boto3 from 1.17.86 to 1.17.87 (#969)
Bumps [boto3](https://github.com/boto/boto3) from 1.17.86 to 1.17.87.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.17.86...1.17.87)

---
updated-dependencies:
- dependency-name: boto3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-04 09:35:47 +02:00
7b39718bd1 tenants: fix fallback for unittests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-03 22:40:01 +02:00
e9621bae06 tests: show logs for containers on failed e2e tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-03 22:17:18 +02:00
0eaabbc0f3 admin: fix upgrading deletion of tasks when listing
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-03 17:42:13 +02:00
5e3628bea6 core: add fallback URLs for websocket to cleanup test logs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-03 17:26:18 +02:00
290ebef8e3 core: instead of migrating sessions, clear cache on initial upgrade
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-03 17:20:25 +02:00
46ab1d20df stages/email: fix token being created without identifier
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-03 14:54:07 +02:00
48e68d6852 core: fix token identifier not being set to unique
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-03 11:45:48 +02:00
cde056825e build(deps): bump @sentry/browser from 6.5.0 to 6.5.1 in /web (#962)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 6.5.0 to 6.5.1.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.5.0...6.5.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-03 10:25:31 +02:00
de25b64f2b build(deps): bump django from 3.2.3 to 3.2.4 (#964)
Bumps [django](https://github.com/django/django) from 3.2.3 to 3.2.4.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.3...3.2.4)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-03 10:00:12 +02:00
32f0c6abe1 build(deps): bump boto3 from 1.17.85 to 1.17.86 (#963)
Bumps [boto3](https://github.com/boto/boto3) from 1.17.85 to 1.17.86.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.17.85...1.17.86)

---
updated-dependencies:
- dependency-name: boto3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-03 09:59:49 +02:00
960210f351 build(deps): bump @sentry/tracing from 6.5.0 to 6.5.1 in /web (#961)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 6.5.0 to 6.5.1.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.5.0...6.5.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-03 09:59:39 +02:00
7c300f0858 web/admin: fix flow export button not working
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-03 00:40:07 +02:00
ed3859800c core: improve API validation for Application's set_icon_url (fix JSON Syntax Error)
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-03 00:39:56 +02:00
06b7f62a40 core: make app's meta_launch_url textfield
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-03 00:21:20 +02:00
45b7c349f1 Merge branch 'next' 2021-06-02 23:46:40 +02:00
7bef6f7153 Zabbix Integration Instructions (#960) 2021-06-02 23:16:52 +02:00
d32e40b1f8 tenants: fix unittests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 22:38:30 +02:00
cec47c3cfc providers/oauth2: show id_token issues for refresh token
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 22:05:04 +02:00
4d773274d4 web: fix missing default favicon and not updating correctly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 21:47:56 +02:00
3ea2b16a12 tenants: add separate field for favicon url
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 21:31:04 +02:00
974ddc07f7 web: improve loading of custom favicon
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 21:19:39 +02:00
2f64b76eba flows: fix invalid background URL when using manually set static or http
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 21:09:54 +02:00
a113778ca7 web/admin: fix css for dropdown toggles which are secondary
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 20:42:31 +02:00
06caaa7c80 web/admin: fix BoundPoliciesList's policy edit button
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 20:29:34 +02:00
b50ac96605 providers/oauth2: remove size limit on Access code nonce
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 20:20:07 +02:00
166b98fa34 web/admin: fix BoundPoliciesList's edit policy button not working
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 18:00:26 +02:00
6d0e0cbe5a outposts: improve validation of providers (must match outpost type)
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 16:04:41 +02:00
b339452843 web: set favicon based on current tenants branding logo
closes #956

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 14:38:24 +02:00
4f04ab7a5f sources/oauth: fix azure AD get_profile_info not working
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 11:34:59 +02:00
35bcd5d174 sources/oauth: improve debug logging
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 11:34:18 +02:00
644ff4a90c outposts: fix error when validating kubeconfig
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-02 10:25:59 +02:00
05d45383be build(deps): bump drf-spectacular from 0.16.0 to 0.17.0 (#957)
Bumps [drf-spectacular](https://github.com/tfranzel/drf-spectacular) from 0.16.0 to 0.17.0.
- [Release notes](https://github.com/tfranzel/drf-spectacular/releases)
- [Changelog](https://github.com/tfranzel/drf-spectacular/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tfranzel/drf-spectacular/compare/0.16.0...0.17.0)

---
updated-dependencies:
- dependency-name: drf-spectacular
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-02 09:25:28 +02:00
702fdfedb7 build(deps): bump uvicorn from 0.13.4 to 0.14.0 (#958)
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.13.4 to 0.14.0.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.13.4...0.14.0)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-02 09:24:46 +02:00
2a0af8750d build(deps): bump boto3 from 1.17.84 to 1.17.85 (#959)
Bumps [boto3](https://github.com/boto/boto3) from 1.17.84 to 1.17.85.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.17.84...1.17.85)

---
updated-dependencies:
- dependency-name: boto3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-02 09:24:18 +02:00
770316a49f web/admin: fix oauth source not creatable
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-01 23:36:21 +02:00
85d349e776 web/admin: only allow outpost providers matching outpost type
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-01 23:35:36 +02:00
f29344e91f web/admin: improve error handling for non-rest_framework errors
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-01 23:34:16 +02:00
9900cc5c81 web/admin: fix urls not showing when pre-selected provider is used
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-01 23:33:59 +02:00
3af48a81e2 web/admin: fix source type not always matching modelname
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-01 23:30:04 +02:00
5bebf26908 web/admin: fix CanSaveMedia -> SaveMedia
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-01 23:16:21 +02:00
eea831fb5a build(deps): bump @typescript-eslint/parser in /web (#952)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.25.0 to 4.26.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.26.0/packages/parser)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-01 10:02:48 +02:00
2e4a9219a2 build(deps): bump @sentry/tracing from 6.4.1 to 6.5.0 in /web (#951)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 6.4.1 to 6.5.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.4.1...6.5.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-01 10:02:34 +02:00
7f1098ce9b build(deps): bump @sentry/browser from 6.4.1 to 6.5.0 in /web (#953)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 6.4.1 to 6.5.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.4.1...6.5.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-01 09:27:49 +02:00
6cd6224d2b build(deps): bump @typescript-eslint/eslint-plugin in /web (#954)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.25.0 to 4.26.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.26.0/packages/eslint-plugin)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-01 09:27:38 +02:00
43d85f8696 build(deps-dev): bump pylint from 2.8.2 to 2.8.3 (#955)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.8.2 to 2.8.3.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.8.2...v2.8.3)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-01 09:27:29 +02:00
ef8b26db13 Fix typo in migrations for authenticator_webauthn (#950) 2021-06-01 00:24:20 +02:00
ebfa7c8dce website/docs: fix docs for outpost annotations
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-31 23:32:17 +02:00
e295f18e78 web/admin: fix error when importing SAML Metadata
closes #948

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-31 22:26:24 +02:00
cef5c2b084 website/docs: add note for CSRF in hass
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-31 17:08:39 +02:00
e24a9e3119 policies: fix missing negate flag of policy bindings
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-31 11:50:29 +02:00
264a170a7e build(deps): bump rollup from 2.50.2 to 2.50.5 in /web (#945)
Bumps [rollup](https://github.com/rollup/rollup) from 2.50.2 to 2.50.5.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.50.2...v2.50.5)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-31 09:06:21 +02:00
8e1c2d7fc0 build(deps): bump @babel/preset-env from 7.14.2 to 7.14.4 in /web (#946)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.14.2 to 7.14.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.14.4/packages/babel-preset-env)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-31 09:06:07 +02:00
6c7f4197a1 build(deps): bump chart.js from 3.3.0 to 3.3.2 in /web (#944)
Bumps [chart.js](https://github.com/chartjs/Chart.js) from 3.3.0 to 3.3.2.
- [Release notes](https://github.com/chartjs/Chart.js/releases)
- [Commits](https://github.com/chartjs/Chart.js/compare/v3.3.0...v3.3.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-31 09:05:54 +02:00
1cd3866855 build(deps): bump django-redis from 4.12.1 to 5.0.0 (#947)
Bumps [django-redis](https://github.com/jazzband/django-redis) from 4.12.1 to 5.0.0.
- [Release notes](https://github.com/jazzband/django-redis/releases)
- [Changelog](https://github.com/jazzband/django-redis/blob/master/CHANGES.rst)
- [Commits](https://github.com/jazzband/django-redis/commits)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-31 09:05:03 +02:00
6a9c95c593 root: generate API Client in dockerfile instead of copying it (#942)
* root: generate API Client in dockerfile instead of copying it

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

* outpost: fix docker build

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

* root: fix path for docker build

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

* outpost: set explicit buildContext

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 17:28:58 +02:00
80adafdb48 admin: fix attribute error when loading old taskinfo
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 16:22:13 +02:00
72f5a4c460 outposts: fix possible recursion error in docker controller
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 16:22:02 +02:00
fb6242d2d3 Merge pull request #941 from goauthentik/authenticated-sessions
Session management
2021-05-30 15:12:49 +02:00
b9773d39c0 core: add tests for authenticated sessions
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 14:43:00 +02:00
0e8d9aa45d api: add System info API
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 14:01:20 +02:00
fc45d35699 core: add migration for sessions
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 13:08:29 +02:00
7e8044619c lib: return default IP if none could be extracted
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 12:49:44 +02:00
cf57660772 web/admin: add basic session management UI
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 01:02:20 +02:00
66a04aeec5 api: add can_geo_ip capability
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 01:02:03 +02:00
73338bdf32 core: add geo_ip to authenticated sessions if enabled
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 01:01:20 +02:00
059da74d1c core: add current attribute to authenticated_session API
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 00:31:41 +02:00
45b8b1e198 core: delete AuthenticatedSession on logout
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 00:27:33 +02:00
5e43eb9838 Merge branch 'master' into authenticated-sessions 2021-05-30 00:23:09 +02:00
11607622a3 web/admin: fix proxy provider's certificate not being optional
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 00:22:58 +02:00
133fc38c05 core: initial authenticated sessions
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-30 00:15:16 +02:00
f51ab7a878 policies/reputation: fix tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 23:46:13 +02:00
c89b8a5f7c web/admin: add UI for reputations
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 23:01:45 +02:00
31ad09c391 stages/identification: add signal which is sent upon identification failure
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 22:58:32 +02:00
05b3c4ddb3 policies/reputation: save username instead of user object
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 22:49:58 +02:00
d52cc30341 website/docs: fix website build
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 22:00:47 +02:00
d2e9683411 website/docs: add tenants
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 21:56:09 +02:00
a4c28a28b4 website/docs: improve docs for expressions
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 21:47:35 +02:00
6232333a52 Merge pull request #940 from goauthentik/tenant
Tenancy
2021-05-29 21:22:10 +02:00
a1203cf4b2 flows: fix ToDefaultFlow not using tenants
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 20:32:42 +02:00
8427fb87f6 tenants: add tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 19:47:55 +02:00
e3578eb7ae Merge branch 'master' into tenant 2021-05-29 19:17:23 +02:00
5990b8d4de outposts: fix docker container not being stopped correctly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 19:16:39 +02:00
3b31b7ce83 core: add http host in log messages
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 19:07:54 +02:00
4d9b362dbf tenants: add migration to add default tenant
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 19:06:35 +02:00
7bd93ed18e web/admin: add webui for tenants
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 18:55:08 +02:00
477ff85109 flows: migrate flow_unenrollment to tenant
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 18:44:02 +02:00
fae8b80ceb core: fix usage of config on templates
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 18:31:05 +02:00
df92f01719 flows: remove default-recovery
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 18:30:55 +02:00
9dd6b7d436 flows: remove default-enrollment
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 18:14:37 +02:00
14f85ec980 tenants: migrate context_processor to tenants
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 18:01:48 +02:00
ff611f21cd tenants: initial implementation
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-29 17:47:25 +02:00
a1b6e09e8a outposts: set restart-policy on docker container
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-28 17:18:11 +02:00
02b5742228 stages/authenticator_duo: add default setup flow
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-28 16:47:39 +02:00
c5cc84c8b6 build(deps-dev): bump requests-mock from 1.9.2 to 1.9.3 (#939) 2021-05-28 09:35:39 +02:00
109ada570f build(deps): bump boto3 from 1.17.82 to 1.17.83 (#938) 2021-05-28 09:35:32 +02:00
b9436c281a build(deps): bump rollup from 2.50.1 to 2.50.2 in /web (#937) 2021-05-28 09:35:20 +02:00
89f2f920cf build(deps): bump boto3 from 1.17.80 to 1.17.82 (#935) 2021-05-27 09:46:57 +02:00
abd0d585a6 build(deps): bump typescript from 4.2.4 to 4.3.2 in /web (#932) 2021-05-27 09:26:26 +02:00
ee74281537 build(deps): bump rollup from 2.50.0 to 2.50.1 in /web (#933) 2021-05-27 09:26:09 +02:00
5488db3574 build(deps): bump docker/setup-qemu-action from 1.1.0 to 1.2.0 (#934) 2021-05-27 09:25:50 +02:00
61f92095a5 build(deps): bump urllib3 from 1.26.4 to 1.26.5 (#936) 2021-05-27 09:25:19 +02:00
3a9f081e1b web/admin: improve persistence for test forms
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-26 12:08:11 +02:00
a237ae3363 web/admin: add Modal to check application access for any user
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-26 12:05:17 +02:00
523621daa2 core: make application's check_access API return a PolicyResult and accept for_user as superuser
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-26 11:47:23 +02:00
309d80a921 ci: build image with timestamp for flux
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-26 10:37:33 +02:00
1bd41116a4 Merge branch 'master' into next 2021-05-26 10:28:03 +02:00
a7b85aeda2 build(deps): bump rollup from 2.49.0 to 2.50.0 in /web (#929)
Bumps [rollup](https://github.com/rollup/rollup) from 2.49.0 to 2.50.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.49.0...v2.50.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-26 10:22:04 +02:00
142861e3ee Merge pull request #930 from goauthentik/dependabot/pip/boto3-1.17.80
build(deps): bump boto3 from 1.17.79 to 1.17.80
2021-05-26 10:21:55 +02:00
02411bb543 tests/e2e: fix IdentificationStage not having sources set
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-25 17:35:24 +02:00
c4453f38a2 stages/identification: make shown sources configurable
closes #918

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-25 16:37:53 +02:00
250e23408e Duo (#917)
* stages/authenticator_duo: initial duo stage

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

* stages/authenticator_duo: improve setup

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

* stages/authenticator_validate: add Duo support

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

* flows: make use of oneOf OpenAPI to annotate all challenge types

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

* outpost: update to new api schema

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

* outposts/ldap: fix client usage

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

* outposts/ldap: return user info when user can't search

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

* web: fix linting error

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

* stages: fix stage unittests

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

* flows: add default challenge response

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

* web/flows: update types

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

* flows: fix mismatched names

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

* website/docs: add duo docs

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

* stages/authenticator_duo: add missing duo device

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

* web/admin: fix enable buttons missing on stages

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

* stages/prompt: annotate PromptChallengeResponse's additionalProperties

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

* web/flows: improve logging

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

* tests/e2e: fix flow titles not being set

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

* tests/e2e: fix invalid flows

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

* api: fix error when authorization header has no spaces

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

* stages/user_write: handle integrity error

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

* outposts: handle disconnects without outpost better

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

* flows: allow blank on WithUserInfo

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-25 13:35:59 +02:00
6f3eb4c068 flows: allow blank on WithUserInfo
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-25 12:53:48 +02:00
58a4b20297 outposts: handle disconnects without outpost better
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-25 12:06:55 +02:00
6d3e067a2b stages/user_write: handle integrity error
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-25 11:46:15 +02:00
6db2bf2a21 api: fix error when authorization header has no spaces
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-25 11:40:49 +02:00
6893948fa0 tests/e2e: fix invalid flows
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-25 11:18:47 +02:00
6317a8c5d0 Merge branch 'master' into duo
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	Pipfile.lock
2021-05-25 09:58:38 +02:00
bc39320f86 tests/e2e: fix flow titles not being set
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-25 09:50:03 +02:00
2001cf0e04 build(deps): bump @typescript-eslint/eslint-plugin in /web (#925)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.24.0 to 4.25.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.25.0/packages/eslint-plugin)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-25 09:24:44 +02:00
712c5df5b1 build(deps): bump @typescript-eslint/parser in /web (#926)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.24.0 to 4.25.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.25.0/packages/parser)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-25 09:20:21 +02:00
8057c63cb4 build(deps): bump chart.js from 3.2.1 to 3.3.0 in /web (#927)
Bumps [chart.js](https://github.com/chartjs/Chart.js) from 3.2.1 to 3.3.0.
- [Release notes](https://github.com/chartjs/Chart.js/releases)
- [Commits](https://github.com/chartjs/Chart.js/compare/v3.2.1...v3.3.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-25 09:20:10 +02:00
7816a3075a build(deps): bump boto3 from 1.17.78 to 1.17.79 (#928)
Bumps [boto3](https://github.com/boto/boto3) from 1.17.78 to 1.17.79.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.17.78...1.17.79)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-25 09:19:42 +02:00
1679e94956 web/flows: improve logging
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 23:43:48 +02:00
8ecac59eca stages/prompt: annotate PromptChallengeResponse's additionalProperties
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 22:55:24 +02:00
af504e13a2 web/admin: fix enable buttons missing on stages
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 22:25:38 +02:00
8183a51b72 stages/authenticator_duo: add missing duo device
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 22:16:29 +02:00
ab25610643 website/docs: add duo docs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 22:11:16 +02:00
127ebed5c6 flows: fix mismatched names
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 21:09:18 +02:00
716923e17a web/flows: update types
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 20:59:44 +02:00
c6bb6709fd flows: add default challenge response
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 20:27:50 +02:00
fb4e0723ee stages: fix stage unittests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 17:12:48 +02:00
8ecacb319c web: fix linting error
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 16:31:05 +02:00
2a5926608f outposts/ldap: return user info when user can't search
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 16:09:05 +02:00
763c3fcfe0 outposts/ldap: fix client usage
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 15:58:59 +02:00
1b346866da Merge branch 'master' into duo
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	Pipfile.lock
2021-05-24 14:54:24 +02:00
25a88c17d1 outpost: update to new api schema
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 14:41:40 +02:00
6f6ae7831e flows: make use of oneOf OpenAPI to annotate all challenge types
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-24 14:11:23 +02:00
0062872e18 build(deps): bump celery from 5.0.5 to 5.1.0 (#921) 2021-05-24 12:51:24 +02:00
e49fb3295f build(deps): bump eslint from 7.26.0 to 7.27.0 in /web (#920) 2021-05-24 12:49:36 +02:00
0e89353ac9 build(deps): bump boto3 from 1.17.77 to 1.17.78 (#924) 2021-05-24 12:49:03 +02:00
b8f98881fa build(deps): bump rollup from 2.48.0 to 2.49.0 in /web (#919) 2021-05-24 12:48:53 +02:00
f887850b95 build(deps): bump github.com/getsentry/sentry-go in /outpost (#922) 2021-05-24 12:48:45 +02:00
2ec4b4ec98 build(deps): bump django-guardian from 2.3.0 to 2.4.0 (#923) 2021-05-24 12:48:23 +02:00
c98e4196bd website/docs: ingress nginx auth headers (#916)
Extend example how to pass through auth headers from authentik if using ingress nginx as forward auth.
2021-05-23 22:49:31 +02:00
3b41c662ed stages/authenticator_validate: add Duo support
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-23 22:31:12 +02:00
65522186f1 stages/authenticator_duo: improve setup
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-23 21:44:52 +02:00
9f5a3c396d stages/authenticator_duo: initial duo stage
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-23 21:10:39 +02:00
53e2b2c784 Prometheus metrics (#914)
* admin: add worker metrics

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

* admin: add version metrics

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

* events: add gauge for system tasks

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

* outposts: add gauge for last hello and connection status

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

* root: re-add prometheus metrics to database

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

* root: allow access to metrics without credentials when debug is on

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

* root: add UpdatingGauge to auto-set value on load

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

* flows: add metrics for cache and building

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

* policies: add metrics for policy engine

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

* events: add histogram for task durations

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

* events: revert to gauge because values are updated on export view

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

* core: add gauge to count all models

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

* events: add metrics for events

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-23 20:29:34 +02:00
a5cd9fa141 outposts: improve logging for docker controller
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-23 19:27:24 +02:00
039a1e544e outpost: use same http client for api requests and oauth token redeeming
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-23 19:27:08 +02:00
0768b201a7 Merge branch 'version-2021.5' 2021-05-22 20:47:48 +02:00
c1c55a6005 lifecycle: fix permission error with local docker
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-22 20:47:05 +02:00
0144e1ad72 Merge branch 'next' 2021-05-22 20:27:26 +02:00
2d5c45543b release: 2021.5.4 2021-05-22 20:15:23 +02:00
9b57f0b81d Merge branch 'version-2021.5' into next
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	web/src/locales/en.po
#	web/src/locales/pseudo-LOCALE.po
2021-05-22 20:01:16 +02:00
9d476a42d1 web: don't set X-Forwarded-Proto when no request TLS Options are set
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-22 19:46:40 +02:00
2c816e6162 providers/proxy: don't use https to communicate with outpost
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-22 18:56:38 +02:00
934cfa483c website/docs: add release notes for 2021.5.4
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-22 13:11:06 +02:00
50308510b4 Merge branch 'version-2021.5' into next
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	web/src/locales/en.po
#	web/src/locales/pseudo-LOCALE.po
2021-05-22 13:10:04 +02:00
dbcb4d46ba web: fix missing flow and policy cache UI
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-22 13:04:13 +02:00
bb89b9b572 Merge branch 'version-2021.5' into next 2021-05-21 23:50:43 +02:00
6600da7d98 providers/oauth2: add missing kid header to JWT Tokens
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-21 23:40:00 +02:00
1a0f72d0a8 Merge branch 'version-2021.5' into next
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	authentik/stages/authenticator_static/api.py
#	swagger.yaml
2021-05-21 21:33:18 +02:00
a265dd54cc stages/authenticator_*: fix Permission Error when disabling Authenticator as non-superuser
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-21 21:25:03 +02:00
a603f42cc0 api: add OwnerFilter
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-21 20:46:59 +02:00
d9a788aac8 api: rename auth to authentication, add authorization for rest_framework permission class
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-21 20:14:03 +02:00
7c6185b581 api: fix URL names for admin Authenticator Views
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-21 19:53:40 +02:00
41a1305555 policies: improve debug logging
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-21 19:10:47 +02:00
75f252b530 flows: rename oob to oobe
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-21 19:10:42 +02:00
c526e5fb9a policies: improve debug logging
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-21 19:10:15 +02:00
7aa903d715 website/docs: update k8s install docs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-21 19:02:05 +02:00
b826eb264e flows: rename oob to oobe
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-21 12:06:39 +02:00
a9519a4a68 g: set x-forwarded-proto based on upstream TLS Status
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-21 09:41:39 +02:00
a4960064c9 build(deps): bump postcss from 8.2.15 to 8.3.0 in /website (#911)
Bumps [postcss](https://github.com/postcss/postcss) from 8.2.15 to 8.3.0.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.2.15...8.3.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-21 09:33:19 +02:00
94bddb9886 build(deps): bump codemirror from 5.61.0 to 5.61.1 in /web (#912)
Bumps [codemirror](https://github.com/codemirror/CodeMirror) from 5.61.0 to 5.61.1.
- [Release notes](https://github.com/codemirror/CodeMirror/releases)
- [Changelog](https://github.com/codemirror/CodeMirror/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codemirror/CodeMirror/compare/5.61.0...5.61.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-21 09:33:08 +02:00
f38702f361 build(deps): bump boto3 from 1.17.76 to 1.17.77 (#913)
Bumps [boto3](https://github.com/boto/boto3) from 1.17.76 to 1.17.77.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.17.76...1.17.77)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-21 09:32:59 +02:00
c49fac39b1 g: set x-forwarded-proto based on upstream TLS Status
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-21 00:03:02 +02:00
b3390f0ab4 website/docs: update nginx config
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 22:06:55 +02:00
7666c246c3 Merge branch 'version-2021.5' 2021-05-20 20:46:18 +02:00
bf4cbb25fe release: 2021.5.3 2021-05-20 20:17:39 +02:00
a925418f60 lib: don't send ImproperlyConfigured to sentry
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 19:18:35 +02:00
ffd61d0e60 root: fix bumpversion
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 19:16:23 +02:00
13cc33c39c website/docs: add 2021.5.3 changelog
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 19:14:28 +02:00
71d112bdcf sources/plex: remove default for plex_token
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 19:13:54 +02:00
c58fe18b97 web: remove nginx config, add caching headers to g
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 19:11:55 +02:00
d2c06c40ea sources/plex: remove default for plex_token
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 19:09:51 +02:00
590c7f4c9d outposts: fix error on outpost disconnect
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 18:07:27 +02:00
9a48c2fd9a outposts: fix error on outpost disconnect
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 18:07:07 +02:00
be5a6c0310 api: add set_*_url method for Application and Flow to set icon/background to URL
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 17:18:40 +02:00
92106ca4bf api: add capabilities to API, add can_save_media
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 17:00:47 +02:00
56f1204c9b outposts: fix update signal not being sent to correct instances
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 15:23:38 +02:00
f6f93640c5 outposts: fix update signal not being sent to correct instances
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 15:23:18 +02:00
b8c76eaf1c build(deps): bump @sentry/tracing from 6.4.0 to 6.4.1 in /web (#908)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 6.4.0 to 6.4.1.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.4.0...6.4.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-20 10:13:43 +02:00
9dbbd4eff6 build(deps): bump @sentry/browser from 6.4.0 to 6.4.1 in /web (#907) 2021-05-20 10:08:47 +02:00
2908be5272 build(deps): bump boto3 from 1.17.75 to 1.17.76 (#909) 2021-05-20 10:08:37 +02:00
349a5b2d00 web/admin: fix flow form not loading data
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 01:10:19 +02:00
63e3667e82 web: fix t.reset is not a function
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 01:10:11 +02:00
92f2a82c03 providers/oauth2: fix double login required when prompt=login
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 01:10:08 +02:00
dcf074650e providers/proxy: fix redirect_uris not always being set on save
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 01:10:04 +02:00
1324ec5146 web/admin: fix flow form not loading data
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 01:09:50 +02:00
0f556fe8a3 web: remove po lineNumbers
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-20 00:06:10 +02:00
19371dad65 web: fix t.reset is not a function
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-19 23:37:23 +02:00
acf1ad91d9 providers/oauth2: fix double login required when prompt=login
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-19 23:34:27 +02:00
a74419214c providers/proxy: fix redirect_uris not always being set on save
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-19 23:10:00 +02:00
7bd8110984 build(deps): bump @lingui/macro from 3.8.10 to 3.9.0 in /web (#902) 2021-05-19 14:50:26 +02:00
aa5623772c build(deps): bump @lingui/core from 3.8.10 to 3.9.0 in /web (#905) 2021-05-19 14:47:43 +02:00
50ede4cc2c build(deps): bump @lingui/cli from 3.8.10 to 3.9.0 in /web (#903)
Bumps [@lingui/cli](https://github.com/lingui/js-lingui) from 3.8.10 to 3.9.0.
- [Release notes](https://github.com/lingui/js-lingui/releases)
- [Changelog](https://github.com/lingui/js-lingui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lingui/js-lingui/compare/v3.8.10...v3.9.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-19 07:35:34 +02:00
879ad27602 build(deps): bump @patternfly/patternfly from 4.102.2 to 4.103.6 in /web (#904)
Bumps [@patternfly/patternfly](https://github.com/patternfly/patternfly) from 4.102.2 to 4.103.6.
- [Release notes](https://github.com/patternfly/patternfly/releases)
- [Changelog](https://github.com/patternfly/patternfly/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/patternfly/patternfly/compare/prerelease-v4.102.2...prerelease-v4.103.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-19 07:35:21 +02:00
37a63d104f build(deps): bump boto3 from 1.17.74 to 1.17.75 (#906)
Bumps [boto3](https://github.com/boto/boto3) from 1.17.74 to 1.17.75.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.17.74...1.17.75)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-19 07:35:07 +02:00
bc6aef7af2 lib: improve sentry integration
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-18 11:29:36 +02:00
2498e72f5d web: remove nginx config, add caching headers to g
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-18 10:15:52 +02:00
c61442c121 build(deps): bump @typescript-eslint/eslint-plugin in /web (#895)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.23.0 to 4.24.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.24.0/packages/eslint-plugin)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-18 09:30:24 +02:00
2d66837742 build(deps): bump @sentry/tracing from 6.3.6 to 6.4.0 in /web (#896)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 6.3.6 to 6.4.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.3.6...6.4.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-18 09:30:16 +02:00
90e7fbe238 build(deps): bump @babel/core from 7.14.2 to 7.14.3 in /web (#897)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.14.2 to 7.14.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.14.3/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-18 09:24:19 +02:00
4447f737e8 build(deps): bump @typescript-eslint/parser in /web (#898)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.23.0 to 4.24.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.24.0/packages/parser)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-18 09:24:10 +02:00
c13c747263 build(deps): bump @babel/plugin-transform-runtime in /web (#899)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.14.2 to 7.14.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.14.3/packages/babel-plugin-transform-runtime)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-18 09:24:00 +02:00
cac23f2fa4 Merge pull request #900 from goauthentik/dependabot/npm_and_yarn/web/sentry/browser-6.4.0
build(deps): bump @sentry/browser from 6.3.6 to 6.4.0 in /web
2021-05-18 09:23:48 +02:00
788ea46d8c flows: fix formatting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-18 09:23:22 +02:00
c285c6b476 Merge pull request #901 from goauthentik/dependabot/pip/boto3-1.17.74 2021-05-18 08:31:48 +02:00
a7cf364e43 build(deps): bump boto3 from 1.17.73 to 1.17.74
Bumps [boto3](https://github.com/boto/boto3) from 1.17.73 to 1.17.74.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.17.73...1.17.74)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-18 04:29:36 +00:00
06dee5d5d8 flows: fix lint error
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-17 23:38:31 +02:00
3cf0f07baf *: fix API Schema for file uploads
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-17 23:12:52 +02:00
e177ab33e0 root: Merge pull request #886 from goauthentik/openapi-v3
OpenAPI v3
2021-05-17 21:25:07 +02:00
9e7c9ae649 web: fix API Path
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-17 20:48:58 +02:00
f016095891 Merge branch 'master' into openapi-v3
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	Pipfile.lock
2021-05-17 20:37:18 +02:00
5a465fbc36 release: 2021.5.2 2021-05-17 19:54:10 +02:00
7cd80a903a build(deps): bump eslint-plugin-lit from 1.4.0 to 1.4.1 in /web (#890)
Bumps [eslint-plugin-lit](https://github.com/43081j/eslint-plugin-lit) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/43081j/eslint-plugin-lit/releases)
- [Commits](https://github.com/43081j/eslint-plugin-lit/compare/v1.4.0...v1.4.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-17 09:12:14 +02:00
dd00351bc7 build(deps): bump rollup from 2.47.0 to 2.48.0 in /web (#889)
Bumps [rollup](https://github.com/rollup/rollup) from 2.47.0 to 2.48.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.47.0...v2.48.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-17 09:12:05 +02:00
5fca7d11b8 build(deps): bump boto3 from 1.17.72 to 1.17.73 (#891)
Bumps [boto3](https://github.com/boto/boto3) from 1.17.72 to 1.17.73.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.17.72...1.17.73)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-17 09:08:53 +02:00
0ff59636f7 build(deps-dev): bump pytest-django from 4.2.0 to 4.3.0 (#892)
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/master/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v4.2.0...v4.3.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-17 09:08:41 +02:00
c4751e4b59 Merge branch 'master' into openapi-v3 2021-05-17 00:12:30 +02:00
e5ebe390d2 ci: fix missing dependencies for scripts.generate_ci_config
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-17 00:08:45 +02:00
7f4bd27b85 Merge branch 'master' into openapi-v3 2021-05-16 23:51:45 +02:00
b66626f9c4 ci: generate secert_key for CI runs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 23:46:23 +02:00
a51a18f3a3 root: remove swagger
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 23:24:52 +02:00
b13d6deda8 root: explicitly set --user for codegen
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 23:14:51 +02:00
23123c43ee website/docs: improve wording on release notes, point to tag for docker-compose download
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 23:08:11 +02:00
8ce918d527 website/docs: Always point to master copy of docker-compose.yml in installation instructions (#888) 2021-05-16 23:02:16 +02:00
626006725e ci: always use makefile
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 22:57:02 +02:00
f9ce41229d api: fix unittests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 22:36:26 +02:00
ae6a406b1d Merge branch 'master' into openapi-v3 2021-05-16 22:29:39 +02:00
45c1a603e7 root: fix linting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 22:29:28 +02:00
330219e76f Merge branch 'master' into openapi-v3 2021-05-16 22:26:07 +02:00
583271d5ed root: only load debug secret key when debug is enabled
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 22:25:55 +02:00
176360fdd7 website/docs: fix $auth_cookie not being defined in outpost docs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 22:18:31 +02:00
0db17b9729 root: remove yasg
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 22:18:04 +02:00
9f9ee66cc4 api: fix linting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 21:57:04 +02:00
ab2bd622a8 Merge branch 'master' into openapi-v3
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	outpost/pkg/ak/api.go
#	outpost/pkg/ak/global.go
#	outpost/pkg/ldap/instance_bind.go
2021-05-16 21:36:24 +02:00
6bd27d27ec outpost: use shorter API Calls
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 21:35:23 +02:00
8d2a3b67b9 lib: Fix config loading of secrets from files (#887) 2021-05-16 21:10:31 +02:00
a5233f89b2 outpost: migrate to openapitools/openapi-generator-cli
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 21:07:01 +02:00
8b6292b3de api: don't overwrite 400 and 403
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 20:54:58 +02:00
cbed5a6522 api: fix missing error definitions
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 19:53:04 +02:00
589f806b7c flows: fix schema for flow executor
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 19:13:31 +02:00
07dc648470 web: fix mixed Static/TOTP pages
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 18:59:33 +02:00
41f6d3b6e7 stages/authenticator_static: add serializer for tokens
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 18:55:47 +02:00
ec8490e105 web: fix remaining API Calls
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 18:52:27 +02:00
69668a2a05 web: fix designation for flow lookups
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 18:46:04 +02:00
d0f1daf025 admin: make tasks's retry api not ask for a body
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 18:43:47 +02:00
d38fd603dd web: fix more special API Calls
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 18:41:02 +02:00
ba5374f6e1 web: mass update API calls
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 18:24:15 +02:00
7152d7ee01 outposts: fix schema for outposts health
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 18:10:11 +02:00
ab07113530 admin: migrate WorkerViewSet to APIView
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 18:06:46 +02:00
a7d7b46747 admin: migrate version view to APIView
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 18:00:02 +02:00
dde1dabf97 web: pass 2 migration
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 17:53:59 +02:00
1f05484e3c web/admin: migrate more components
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 17:51:58 +02:00
9a44088d2b admin: migrate metrics viewset to APIView
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 17:49:37 +02:00
b351ae12c5 api: make config viewset single view
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 17:44:19 +02:00
759bf59780 core: make filefields readonly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 17:34:55 +02:00
10cb60f48e api: fix pagination not being required in schema
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 17:32:13 +02:00
99be97206b web: fix enums
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 15:31:13 +02:00
ef9f08553c *: linting pass, rename from swagger to schema
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 15:22:57 +02:00
4fb71a6bdd api: fix pagination schema
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 15:08:51 +02:00
3ab7588b73 web: Read() to Retrieve()
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 14:43:42 +02:00
cac1f242dc *: replace swagger with openapi
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 14:23:05 +02:00
0bac738090 *: fix static response descriptions
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 14:07:29 +02:00
d0d3072c50 outposts/ldap: fix AUTHENTIK_INSECURE not being respected for API client during bind
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-16 00:01:16 +02:00
1324d03815 *: initial migration to openapi v3
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-15 23:57:28 +02:00
34e2bbc41d Merge branch 'next' 2021-05-15 23:25:17 +02:00
ea2dbb2f33 web/admin: fix error when copying token while none exist
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-15 23:25:06 +02:00
c55f2ad10a root: set additional sentry tags
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-15 19:53:43 +02:00
2cde40aeee website/docs: add release notes for 2021.5.2
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-15 17:49:34 +02:00
a30b32fbbf outposts: fix missing default for OutpostState.for_channel
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-15 17:46:53 +02:00
1745306cc6 outposts: fix error when controller loads from cache but cache has expired
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-15 17:45:33 +02:00
8925787a13 flows: fix error when using cancel flow
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-15 17:42:37 +02:00
968b7ec17a lib: fix parsing of remote IP header when behind multiple reverse proxies
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-15 15:08:53 +02:00
6600d5bf69 providers/oauth2: use user.uid
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-15 14:08:49 +02:00
a4278833d8 providers/proxy: fix ingress not being created with full https
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-15 13:45:41 +02:00
942905b9b1 providers/proxy: fix formatting issue
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-14 16:24:35 +02:00
81056c3889 LDAP: use username instead of name for user dn (#883) 2021-05-14 12:58:27 +02:00
36b694fc41 website/docs: add example ldapsearch command
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-14 11:47:38 +02:00
2d9f216658 web/admin: add notice for LDAP Provider's group selection
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-14 11:44:01 +02:00
8d7bb7da17 providers/proxy: connect ingress to https instead of http
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#882
2021-05-14 11:42:03 +02:00
965db6eaf5 outposts/proxy: fix insecure TLS Skip
closes #882

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-14 11:38:40 +02:00
9bdd6f23a4 website/docs: add ldap example, use ghcr
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-14 11:19:09 +02:00
675ad7710c outposts/proxy: fix error redeeming code when using non-standard ports
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-14 11:13:57 +02:00
9939db13c3 outposts: fix reload notification not working due to wrong ID being saved
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-14 11:13:04 +02:00
03e134b296 web/admin: fix propertymappings not loading correctly
closes #879

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-14 10:58:33 +02:00
465750276c core: fix application's slug field not being set to unique
closes #881

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-14 10:49:42 +02:00
9b13191646 web: fix chunks overwriting each other
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-14 01:06:29 +02:00
634ea61b50 lifecycle: check if group of docker socket exists
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-14 00:50:20 +02:00
0fcb4936a2 web: output js chunks without hashing
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-13 23:15:40 +02:00
934e62d5be lifecycle: fix error when worker is not running as root
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-13 22:55:35 +02:00
c5e9197b19 website/docs: fix release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-13 21:43:10 +02:00
513 changed files with 42841 additions and 27687 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2021.5.1
current_version = 2021.6.1-rc4
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
@ -31,8 +31,6 @@ values =
[bumpversion:file:web/src/constants.ts]
[bumpversion:file:web/nginx.conf]
[bumpversion:file:website/docs/outposts/manual-deploy-docker-compose.md]
[bumpversion:file:website/docs/outposts/manual-deploy-kubernetes.md]

27
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@ -0,0 +1,27 @@
---
name: Question
about: Ask a question about a feature or specific configuration
title: ''
labels: question
assignees: ''
---
**Describe your question/**
A clear and concise description of what you're trying to do.
**Relevant infos**
i.e. Version of other software you're using, specifics of your setup
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Output of docker-compose logs or kubectl logs respectively
**Version and Deployment (please complete the following information):**
- authentik version: [e.g. 0.10.0-stable]
- Deployment: [e.g. docker-compose, helm]
**Additional context**
Add any other context about the problem here.

13
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,13 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# 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
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.

View File

@ -14,7 +14,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.1.0
uses: docker/setup-qemu-action@v1.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker Login Registry
@ -28,17 +28,14 @@ jobs:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: prepare ts api client
run: |
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
- name: Building Docker Image
uses: docker/build-push-action@v2
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik:2021.5.1,
beryju/authentik:2021.6.1-rc4,
beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.5.1,
ghcr.io/goauthentik/server:2021.6.1-rc4,
ghcr.io/goauthentik/server:latest
platforms: linux/amd64,linux/arm64
context: .
@ -49,14 +46,8 @@ jobs:
- uses: actions/setup-go@v2
with:
go-version: "^1.15"
- name: prepare go api client
run: |
cd outpost
go get -u github.com/go-swagger/go-swagger/cmd/swagger
swagger generate client -f ../swagger.yaml -A authentik -t pkg/
go build -v ./cmd/proxy/server.go
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.1.0
uses: docker/setup-qemu-action@v1.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker Login Registry
@ -75,11 +66,10 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-proxy:2021.5.1,
beryju/authentik-proxy:2021.6.1-rc4,
beryju/authentik-proxy:latest,
ghcr.io/goauthentik/proxy:2021.5.1,
ghcr.io/goauthentik/proxy:2021.6.1-rc4,
ghcr.io/goauthentik/proxy:latest
context: outpost/
file: outpost/proxy.Dockerfile
platforms: linux/amd64,linux/arm64
build-ldap:
@ -89,14 +79,8 @@ jobs:
- uses: actions/setup-go@v2
with:
go-version: "^1.15"
- name: prepare go api client
run: |
cd outpost
go get -u github.com/go-swagger/go-swagger/cmd/swagger
swagger generate client -f ../swagger.yaml -A authentik -t pkg/
go build -v ./cmd/ldap/server.go
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.1.0
uses: docker/setup-qemu-action@v1.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker Login Registry
@ -115,11 +99,10 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-ldap:2021.5.1,
beryju/authentik-ldap:2021.6.1-rc4,
beryju/authentik-ldap:latest,
ghcr.io/goauthentik/ldap:2021.5.1,
ghcr.io/goauthentik/ldap:2021.6.1-rc4,
ghcr.io/goauthentik/ldap:latest
context: outpost/
file: outpost/ldap.Dockerfile
platforms: linux/amd64,linux/arm64
test-release:
@ -139,7 +122,7 @@ jobs:
docker-compose pull -q
docker-compose up --no-start
docker-compose start postgresql redis
docker-compose run -u root --entrypoint /bin/bash server -c "apt-get update && apt-get install -y --no-install-recommends git && pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik"
docker-compose run -u root server test
sentry-release:
if: ${{ github.event_name == 'release' }}
needs:
@ -155,5 +138,5 @@ jobs:
SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org
with:
version: authentik@2021.5.1
version: authentik@2021.6.1-rc4
environment: beryjuorg-prod

View File

@ -11,9 +11,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: prepare ts api client
run: |
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
- name: Pre-release test
run: |
sudo apt-get install -y pwgen
@ -23,11 +20,11 @@ jobs:
docker-compose pull -q
docker build \
--no-cache \
-t beryju/authentik:latest \
-t ghcr.io/goauthentik/server:latest \
-f Dockerfile .
docker-compose up --no-start
docker-compose start postgresql redis
docker-compose run -u root --entrypoint /bin/bash server -c "apt-get update && apt-get install -y --no-install-recommends git && pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik"
docker-compose run -u root server test
- name: Extract version number
id: get_version
uses: actions/github-script@v4.0.2

View File

@ -8,18 +8,30 @@ WORKDIR /app/
RUN pip install pipenv && \
pipenv lock -r > requirements.txt && \
pipenv lock -rd > requirements-dev.txt
pipenv lock -r --dev-only > requirements-dev.txt
# Stage 2: Build webui
# Stage 2: Build web API
FROM openapitools/openapi-generator-cli as api-builder
COPY ./schema.yml /local/schema.yml
RUN docker-entrypoint.sh generate \
-i /local/schema.yml \
-g typescript-fetch \
-o /local/web/api \
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
# Stage 3: Build webui
FROM node as npm-builder
COPY ./web /static/
COPY --from=api-builder /local/web/api /static/api
ENV NODE_ENV=production
RUN cd /static && npm i --production=false && npm run build
RUN cd /static && npm i && npm run build
# Stage 3: Build go proxy
FROM golang:1.16.4 AS builder
# Stage 4: Build go proxy
FROM golang:1.16.5 AS builder
WORKDIR /work
@ -28,7 +40,6 @@ COPY --from=npm-builder /static/security.txt /work/web/security.txt
COPY --from=npm-builder /static/dist/ /work/web/dist/
COPY --from=npm-builder /static/authentik/ /work/web/authentik/
# RUN ls /work/web/static/authentik/ && exit 1
COPY ./cmd /work/cmd
COPY ./web/static.go /work/web/static.go
COPY ./internal /work/internal
@ -37,7 +48,7 @@ COPY ./go.sum /work/go.sum
RUN go build -o /work/authentik ./cmd/server/main.go
# Stage 4: Run
# Stage 5: Run
FROM python:3.9-slim-buster
WORKDIR /
@ -65,6 +76,7 @@ RUN apt-get update && \
COPY ./authentik/ /authentik
COPY ./pyproject.toml /
COPY ./xml /xml
COPY ./tests /tests
COPY ./manage.py /
COPY ./lifecycle/ /lifecycle
COPY --from=builder /work/authentik /authentik-proxy

View File

@ -1,5 +1,7 @@
.SHELLFLAGS += -x -e
PWD = $(shell pwd)
UID = $(shell id -u)
GID = $(shell id -g)
all: lint-fix lint test gen
@ -25,16 +27,39 @@ lint:
bandit -r authentik tests lifecycle -x node_modules
pylint authentik tests lifecycle
gen:
./manage.py generate_swagger -o swagger.yaml -f yaml
gen-build:
./manage.py spectacular --file schema.yml
gen-clean:
rm -rf web/api/src/
rm -rf outpost/api/
gen-web:
docker run \
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
openapitools/openapi-generator-cli generate \
-i /local/swagger.yaml \
-i /local/schema.yml \
-g typescript-fetch \
-o /local/web/api \
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
cd web/api && npx tsc
gen-outpost:
docker run \
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
openapitools/openapi-generator-cli generate \
--git-host goauthentik.io \
--git-repo-id outpost \
--git-user-id api \
-i /local/schema.yml \
-g go \
-o /local/outpost/api \
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true
rm -f outpost/api/go.mod outpost/api/go.sum
gen: gen-build gen-clean gen-web gen-outpost
run:
go run -v cmd/server/main.go

View File

@ -22,7 +22,7 @@ django-storages = "*"
djangorestframework = "*"
djangorestframework-guardian = "*"
docker = "*"
drf_yasg = "*"
drf-spectacular = "*"
facebook-sdk = "*"
geoip2 = "*"
gunicorn = "*"
@ -44,6 +44,8 @@ urllib3 = {extras = ["secure"],version = "*"}
uvicorn = {extras = ["standard"],version = "*"}
webauthn = "*"
xmlsec = "*"
duo-client = "*"
ua-parser = "*"
[requires]
python_version = "3.9"

503
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,3 @@
"""authentik"""
__version__ = "2021.5.1"
__version__ = "2021.6.1-rc4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -1,5 +1,5 @@
"""Meta API"""
from drf_yasg.utils import swagger_auto_schema
from drf_spectacular.utils import extend_schema
from rest_framework.fields import CharField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
@ -22,7 +22,7 @@ class AppsViewSet(ViewSet):
permission_classes = [IsAdminUser]
@swagger_auto_schema(responses={200: AppSerializer(many=True)})
@extend_schema(responses={200: AppSerializer(many=True)})
def list(self, request: Request) -> Response:
"""List current messages and pass into Serializer"""
data = []

View File

@ -7,12 +7,12 @@ 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_yasg.utils import swagger_auto_schema, swagger_serializer_method
from drf_spectacular.utils import extend_schema, extend_schema_field
from rest_framework.fields import IntegerField, SerializerMethodField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
from rest_framework.views import APIView
from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import Event, EventAction
@ -58,24 +58,24 @@ class LoginMetricsSerializer(PassiveSerializer):
logins_per_1h = SerializerMethodField()
logins_failed_per_1h = SerializerMethodField()
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
@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)
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
@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)
class AdministrationMetricsViewSet(ViewSet):
class AdministrationMetricsViewSet(APIView):
"""Login Metrics per 1h"""
permission_classes = [IsAdminUser]
@swagger_auto_schema(responses={200: LoginMetricsSerializer(many=False)})
def list(self, request: Request) -> Response:
@extend_schema(responses={200: LoginMetricsSerializer(many=False)})
def get(self, request: Request) -> Response:
"""Login Metrics per 1h"""
serializer = LoginMetricsSerializer(True)
return Response(serializer.data)

View File

@ -0,0 +1,91 @@
"""authentik administration overview"""
import os
import platform
from datetime import datetime
from sys import version as python_version
from typing import TypedDict
from django.utils.timezone import now
from drf_spectacular.utils import extend_schema
from gunicorn import version_info as gunicorn_version
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
from rest_framework.fields import SerializerMethodField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from authentik.core.api.utils import PassiveSerializer
class RuntimeDict(TypedDict):
"""Runtime information"""
python_version: str
gunicorn_version: str
environment: str
architecture: str
platform: str
uname: str
class SystemSerializer(PassiveSerializer):
"""Get system information."""
http_headers = SerializerMethodField()
http_host = SerializerMethodField()
http_is_secure = SerializerMethodField()
runtime = SerializerMethodField()
tenant = SerializerMethodField()
server_time = SerializerMethodField()
def get_http_headers(self, request: Request) -> dict[str, str]:
"""Get HTTP Request headers"""
headers = {}
for key, value in request.META.items():
if not isinstance(value, str):
continue
headers[key] = value
return headers
def get_http_host(self, request: Request) -> str:
"""Get HTTP host"""
return request._request.get_host()
def get_http_is_secure(self, request: Request) -> bool:
"""Get HTTP Secure flag"""
return request._request.is_secure()
def get_runtime(self, request: Request) -> RuntimeDict:
"""Get versions"""
return {
"python_version": python_version,
"gunicorn_version": ".".join(str(x) for x in gunicorn_version),
"environment": "kubernetes"
if SERVICE_HOST_ENV_NAME in os.environ
else "compose",
"architecture": platform.machine(),
"platform": platform.platform(),
"uname": " ".join(platform.uname()),
}
def get_tenant(self, request: Request) -> str:
"""Currently active tenant"""
return str(request._request.tenant)
def get_server_time(self, request: Request) -> datetime:
"""Current server time"""
return now()
class SystemView(APIView):
"""Get system information."""
permission_classes = [IsAdminUser]
pagination_class = None
filter_backends = []
@extend_schema(responses={200: SystemSerializer(many=False)})
def get(self, request: Request) -> Response:
"""Get system information."""
return Response(SystemSerializer(request).data)

View File

@ -4,7 +4,8 @@ from importlib import import_module
from django.contrib import messages
from django.http.response import Http404
from django.utils.translation import gettext_lazy as _
from drf_yasg.utils import swagger_auto_schema
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField
from rest_framework.permissions import IsAdminUser
@ -21,7 +22,7 @@ class TaskSerializer(PassiveSerializer):
task_name = CharField()
task_description = CharField()
task_finish_timestamp = DateTimeField(source="finish_timestamp")
task_finish_timestamp = DateTimeField(source="finish_time")
status = ChoiceField(
source="result.status.name",
@ -29,14 +30,32 @@ class TaskSerializer(PassiveSerializer):
)
messages = ListField(source="result.messages")
def to_representation(self, instance):
"""When a new version of authentik adds fields to TaskInfo,
the API will fail with an AttributeError, as the classes
are pickled in cache. In that case, just delete the info"""
try:
return super().to_representation(instance)
except AttributeError:
if isinstance(self.instance, list):
for inst in self.instance:
inst.delete()
else:
self.instance.delete()
return {}
class TaskViewSet(ViewSet):
"""Read-only view set that returns all background tasks"""
permission_classes = [IsAdminUser]
serializer_class = TaskSerializer
@swagger_auto_schema(
responses={200: TaskSerializer(many=False), 404: "Task not found"}
@extend_schema(
responses={
200: TaskSerializer(many=False),
404: OpenApiResponse(description="Task not found"),
}
)
# pylint: disable=invalid-name
def retrieve(self, request: Request, pk=None) -> Response:
@ -46,18 +65,19 @@ class TaskViewSet(ViewSet):
raise Http404
return Response(TaskSerializer(task, many=False).data)
@swagger_auto_schema(responses={200: TaskSerializer(many=True)})
@extend_schema(responses={200: TaskSerializer(many=True)})
def list(self, request: Request) -> Response:
"""List system tasks"""
tasks = sorted(TaskInfo.all().values(), key=lambda task: task.task_name)
return Response(TaskSerializer(tasks, many=True).data)
@swagger_auto_schema(
@extend_schema(
request=OpenApiTypes.NONE,
responses={
204: "Task retried successfully",
404: "Task not found",
500: "Failed to retry task",
}
204: OpenApiResponse(description="Task retried successfully"),
404: OpenApiResponse(description="Task not found"),
500: OpenApiResponse(description="Failed to retry task"),
},
)
@action(detail=True, methods=["post"])
# pylint: disable=invalid-name

View File

@ -2,14 +2,13 @@
from os import environ
from django.core.cache import cache
from drf_yasg.utils import swagger_auto_schema
from drf_spectacular.utils import extend_schema
from packaging.version import parse
from rest_framework.fields import SerializerMethodField
from rest_framework.mixins import ListModelMixin
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from rest_framework.views import APIView
from authentik import ENV_GIT_HASH_KEY, __version__
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
@ -47,17 +46,14 @@ class VersionSerializer(PassiveSerializer):
)
class VersionViewSet(ListModelMixin, GenericViewSet):
class VersionView(APIView):
"""Get running and latest version."""
permission_classes = [IsAuthenticated]
pagination_class = None
filter_backends = []
def get_queryset(self): # pragma: no cover
return None
@swagger_auto_schema(responses={200: VersionSerializer(many=False)})
def list(self, request: Request) -> Response:
@extend_schema(responses={200: VersionSerializer(many=False)})
def get(self, request: Request) -> Response:
"""Get running and latest version."""
return Response(VersionSerializer(True).data)

View File

@ -1,25 +1,26 @@
"""authentik administration overview"""
from rest_framework.mixins import ListModelMixin
from drf_spectacular.utils import extend_schema, inline_serializer
from prometheus_client import Gauge
from rest_framework.fields import IntegerField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import GenericViewSet
from rest_framework.views import APIView
from authentik.root.celery import CELERY_APP
GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers")
class WorkerViewSet(ListModelMixin, GenericViewSet):
class WorkerView(APIView):
"""Get currently connected worker count."""
serializer_class = Serializer
permission_classes = [IsAdminUser]
def get_queryset(self): # pragma: no cover
return None
def list(self, request: Request) -> Response:
@extend_schema(
responses=inline_serializer("Workers", fields={"count": IntegerField()})
)
def get(self, request: Request) -> Response:
"""Get currently connected worker count."""
return Response(
{"pagination": {"count": len(CELERY_APP.control.ping(timeout=0.5))}}
)
count = len(CELERY_APP.control.ping(timeout=0.5))
return Response({"count": count})

View File

@ -1,13 +1,15 @@
"""authentik admin tasks"""
import re
from os import environ
from django.core.cache import cache
from django.core.validators import URLValidator
from packaging.version import parse
from prometheus_client import Info
from requests import RequestException, get
from structlog.stdlib import get_logger
from authentik import __version__
from authentik import ENV_GIT_HASH_KEY, __version__
from authentik.events.models import Event, EventAction
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.root.celery import CELERY_APP
@ -17,6 +19,18 @@ VERSION_CACHE_KEY = "authentik_latest_version"
VERSION_CACHE_TIMEOUT = 8 * 60 * 60 # 8 hours
# Chop of the first ^ because we want to search the entire string
URL_FINDER = URLValidator.regex.pattern[1:]
PROM_INFO = Info("authentik_version", "Currently running authentik version")
def _set_prom_info():
"""Set prometheus info for version"""
PROM_INFO.info(
{
"version": __version__,
"latest": cache.get(VERSION_CACHE_KEY, ""),
"build_hash": environ.get(ENV_GIT_HASH_KEY, ""),
}
)
@CELERY_APP.task(bind=True, base=MonitoredTask)
@ -36,6 +50,7 @@ def update_latest_version(self: MonitoredTask):
TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"]
)
)
_set_prom_info()
# Check if upstream version is newer than what we're running,
# and if no event exists yet, create one.
local_version = parse(__version__)
@ -53,3 +68,6 @@ def update_latest_version(self: MonitoredTask):
except (RequestException, IndexError) as exc:
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
_set_prom_info()

View File

@ -74,24 +74,29 @@ class TestAdminAPI(TestCase):
def test_version(self):
"""Test Version API"""
response = self.client.get(reverse("authentik_api:admin_version-list"))
response = self.client.get(reverse("authentik_api:admin_version"))
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(body["version_current"], __version__)
def test_workers(self):
"""Test Workers API"""
response = self.client.get(reverse("authentik_api:admin_workers-list"))
response = self.client.get(reverse("authentik_api:admin_workers"))
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(body["pagination"]["count"], 0)
self.assertEqual(body["count"], 0)
def test_metrics(self):
"""Test metrics API"""
response = self.client.get(reverse("authentik_api:admin_metrics-list"))
response = self.client.get(reverse("authentik_api:admin_metrics"))
self.assertEqual(response.status_code, 200)
def test_apps(self):
"""Test apps API"""
response = self.client.get(reverse("authentik_api:apps-list"))
self.assertEqual(response.status_code, 200)
def test_system(self):
"""Test system API"""
response = self.client.get(reverse("authentik_api:admin_system"))
self.assertEqual(response.status_code, 200)

View File

@ -3,6 +3,7 @@ from base64 import b64decode
from binascii import Error
from typing import Any, Optional, Union
from drf_spectacular.authentication import OpenApiAuthenticationExtension
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.request import Request
@ -17,7 +18,7 @@ LOGGER = get_logger()
def token_from_header(raw_header: bytes) -> Optional[Token]:
"""raw_header in the Format of `Bearer dGVzdDp0ZXN0`"""
auth_credentials = raw_header.decode()
if auth_credentials == "":
if auth_credentials == "" or " " not in auth_credentials:
return None
auth_type, auth_credentials = auth_credentials.split()
if auth_type.lower() not in ["basic", "bearer"]:
@ -42,7 +43,7 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
return tokens.first()
class AuthentikTokenAuthentication(BaseAuthentication):
class TokenAuthentication(BaseAuthentication):
"""Token-based authentication using HTTP Bearer authentication"""
def authenticate(self, request: Request) -> Union[tuple[User, Any], None]:
@ -55,3 +56,18 @@ class AuthentikTokenAuthentication(BaseAuthentication):
return None
return (token.user, None) # pragma: no cover
class TokenSchema(OpenApiAuthenticationExtension):
"""Auth schema"""
target_class = TokenAuthentication
name = "authentik"
def get_security_definition(self, auto_schema):
"""Auth schema"""
return {
"type": "apiKey",
"in": "header",
"name": "Authorization",
}

View File

@ -0,0 +1,35 @@
"""API Authorization"""
from django.db.models import Model
from django.db.models.query import QuerySet
from rest_framework.filters import BaseFilterBackend
from rest_framework.permissions import BasePermission
from rest_framework.request import Request
class OwnerFilter(BaseFilterBackend):
"""Filter objects by their owner"""
owner_key = "user"
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
return queryset.filter(**{self.owner_key: request.user})
class OwnerPermissions(BasePermission):
"""Authorize requests by an object's owner matching the requesting user"""
owner_key = "user"
def has_permission(self, request: Request, view) -> bool:
"""If the user is authenticated, we allow all requests here. For listing, the
object-level permissions are done by the filter backend"""
return request.user.is_authenticated
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
"""Check if the object's owner matches the currently logged in user"""
if not hasattr(obj, self.owner_key):
return False
owner = getattr(obj, self.owner_key)
if owner != request.user:
return False
return True

View File

@ -30,3 +30,47 @@ class Pagination(pagination.PageNumberPagination):
"results": data,
}
)
def get_paginated_response_schema(self, schema):
return {
"type": "object",
"properties": {
"pagination": {
"type": "object",
"properties": {
"next": {
"type": "number",
},
"previous": {
"type": "number",
},
"count": {
"type": "number",
},
"current": {
"type": "number",
},
"total_pages": {
"type": "number",
},
"start_index": {
"type": "number",
},
"end_index": {
"type": "number",
},
},
"required": [
"next",
"previous",
"count",
"current",
"total_pages",
"start_index",
"end_index",
],
},
"results": schema,
},
"required": ["pagination", "results"],
}

View File

@ -1,97 +0,0 @@
"""Swagger Pagination Schema class"""
from typing import OrderedDict
from drf_yasg import openapi
from drf_yasg.inspectors import PaginatorInspector
class PaginationInspector(PaginatorInspector):
"""Swagger Pagination Schema class"""
def get_paginated_response(self, paginator, response_schema):
"""
:param BasePagination paginator: the paginator
:param openapi.Schema response_schema: the response schema that must be paged.
:rtype: openapi.Schema
"""
return openapi.Schema(
type=openapi.TYPE_OBJECT,
properties=OrderedDict(
(
(
"pagination",
openapi.Schema(
type=openapi.TYPE_OBJECT,
properties=OrderedDict(
(
("next", openapi.Schema(type=openapi.TYPE_NUMBER)),
(
"previous",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
("count", openapi.Schema(type=openapi.TYPE_NUMBER)),
(
"current",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
(
"total_pages",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
(
"start_index",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
(
"end_index",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
)
),
required=[
"next",
"previous",
"count",
"current",
"total_pages",
"start_index",
"end_index",
],
),
),
("results", response_schema),
)
),
required=["results", "pagination"],
)
def get_paginator_parameters(self, paginator):
"""
Get the pagination parameters for a single paginator **instance**.
Should return :data:`.NotHandled` if this inspector
does not know how to handle the given `paginator`.
:param BasePagination paginator: the paginator
:rtype: list[openapi.Parameter]
"""
return [
openapi.Parameter(
"page",
openapi.IN_QUERY,
"Page Index",
False,
None,
openapi.TYPE_INTEGER,
),
openapi.Parameter(
"page_size",
openapi.IN_QUERY,
"Page Size",
False,
None,
openapi.TYPE_INTEGER,
),
]

View File

@ -1,102 +1,77 @@
"""Error Response schema, from https://github.com/axnsan12/drf-yasg/issues/224"""
from drf_yasg import openapi
from drf_yasg.inspectors.view import SwaggerAutoSchema
from drf_yasg.utils import force_real_str, is_list_view
from rest_framework import exceptions, status
from rest_framework.settings import api_settings
from django.utils.translation import gettext_lazy as _
from drf_spectacular.plumbing import (
ResolvedComponent,
build_array_type,
build_basic_type,
build_object_type,
)
from drf_spectacular.settings import spectacular_settings
from drf_spectacular.types import OpenApiTypes
class ErrorResponseAutoSchema(SwaggerAutoSchema):
"""Inspector which includes an error schema"""
def build_standard_type(obj, **kwargs):
"""Build a basic type with optional add ons."""
schema = build_basic_type(obj)
schema.update(kwargs)
return schema
def get_generic_error_schema(self):
"""Get a generic error schema"""
return openapi.Schema(
"Generic API Error",
type=openapi.TYPE_OBJECT,
properties={
"detail": openapi.Schema(
type=openapi.TYPE_STRING, description="Error details"
),
"code": openapi.Schema(
type=openapi.TYPE_STRING, description="Error code"
),
},
required=["detail"],
GENERIC_ERROR = build_object_type(
description=_("Generic API Error"),
properties={
"detail": build_standard_type(OpenApiTypes.STR),
"code": build_standard_type(OpenApiTypes.STR),
},
required=["detail"],
)
VALIDATION_ERROR = build_object_type(
description=_("Validation Error"),
properties={
"non_field_errors": build_array_type(build_standard_type(OpenApiTypes.STR)),
"code": build_standard_type(OpenApiTypes.STR),
},
required=["detail"],
additionalProperties={},
)
def postprocess_schema_responses(result, generator, **kwargs): # noqa: W0613
"""Workaround to set a default response for endpoints.
Workaround suggested at
<https://github.com/tfranzel/drf-spectacular/issues/119#issuecomment-656970357>
for the missing drf-spectacular feature discussed in
<https://github.com/tfranzel/drf-spectacular/issues/101>.
"""
def create_component(name, schema, type_=ResolvedComponent.SCHEMA):
"""Register a component and return a reference to it."""
component = ResolvedComponent(
name=name,
type=type_,
schema=schema,
object=name,
)
generator.registry.register_on_missing(component)
return component
def get_validation_error_schema(self):
"""Get a generic validation error schema"""
return openapi.Schema(
"Validation Error",
type=openapi.TYPE_OBJECT,
properties={
api_settings.NON_FIELD_ERRORS_KEY: openapi.Schema(
description="List of validation errors not related to any field",
type=openapi.TYPE_ARRAY,
items=openapi.Schema(type=openapi.TYPE_STRING),
),
},
additional_properties=openapi.Schema(
description=(
"A list of error messages for each "
"field that triggered a validation error"
),
type=openapi.TYPE_ARRAY,
items=openapi.Schema(type=openapi.TYPE_STRING),
),
)
generic_error = create_component("GenericError", GENERIC_ERROR)
validation_error = create_component("ValidationError", VALIDATION_ERROR)
def get_response_serializers(self):
responses = super().get_response_serializers()
definitions = self.components.with_scope(
openapi.SCHEMA_DEFINITIONS
) # type: openapi.ReferenceResolver
for path in result["paths"].values():
for method in path.values():
method["responses"].setdefault("400", validation_error.ref)
method["responses"].setdefault("403", generic_error.ref)
definitions.setdefault("GenericError", self.get_generic_error_schema)
definitions.setdefault("ValidationError", self.get_validation_error_schema)
definitions.setdefault("APIException", self.get_generic_error_schema)
result["components"] = generator.registry.build(
spectacular_settings.APPEND_COMPONENTS
)
if self.get_request_serializer() or self.get_query_serializer():
responses.setdefault(
exceptions.ValidationError.status_code,
openapi.Response(
description=force_real_str(
exceptions.ValidationError.default_detail
),
schema=openapi.SchemaRef(definitions, "ValidationError"),
),
)
security = self.get_security()
if security is None or len(security) > 0:
# Note: 401 error codes are coerced into 403 see
# rest_framework/views.py:433:handle_exception
# This is b/c the API uses token auth which doesn't have WWW-Authenticate header
responses.setdefault(
status.HTTP_403_FORBIDDEN,
openapi.Response(
description="Authentication credentials were invalid, absent or insufficient.",
schema=openapi.SchemaRef(definitions, "GenericError"),
),
)
if not is_list_view(self.path, self.method, self.view):
responses.setdefault(
exceptions.PermissionDenied.status_code,
openapi.Response(
description="Permission denied.",
schema=openapi.SchemaRef(definitions, "APIException"),
),
)
responses.setdefault(
exceptions.NotFound.status_code,
openapi.Response(
description=(
"Object does not exist or caller "
"has insufficient permissions to access it."
),
schema=openapi.SchemaRef(definitions, "APIException"),
),
)
return responses
# This is a workaround for authentik/stages/prompt/stage.py
# since the serializer PromptChallengeResponse
# accepts dynamic keys
for component in result["components"]["schemas"]:
if component == "PromptChallengeResponseRequest":
comp = result["components"]["schemas"][component]
comp["additionalProperties"] = {}
return result

View File

@ -3,7 +3,7 @@
{% load static %}
{% block title %}
API Browser - {{ config.authentik.branding.title }}
API Browser - {{ tenant.branding_title }}
{% endblock %}
{% block head %}

View File

@ -5,7 +5,7 @@ from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from rest_framework.exceptions import AuthenticationFailed
from authentik.api.auth import token_from_header
from authentik.api.authentication import token_from_header
from authentik.core.models import Token, TokenIntents

View File

@ -11,6 +11,6 @@ class TestConfig(APITestCase):
def test_config(self):
"""Test YAML generation"""
response = self.client.get(
reverse("authentik_api:configs-list"),
reverse("authentik_api:config"),
)
self.assertTrue(loads(response.content.decode()))

View File

@ -0,0 +1,22 @@
"""Schema generation tests"""
from django.urls import reverse
from rest_framework.test import APITestCase
from yaml import safe_load
class TestSchemaGeneration(APITestCase):
"""Generic admin tests"""
def test_schema(self):
"""Test generation"""
response = self.client.get(
reverse("authentik_api:schema"),
)
self.assertTrue(safe_load(response.content.decode()))
def test_browser(self):
"""Test API Browser"""
response = self.client.get(
reverse("authentik_api:schema-browser"),
)
self.assertEqual(response.status_code, 200)

View File

@ -1,31 +0,0 @@
"""Swagger generation tests"""
from json import loads
from django.urls import reverse
from rest_framework.test import APITestCase
from yaml import safe_load
class TestSwaggerGeneration(APITestCase):
"""Generic admin tests"""
def test_yaml(self):
"""Test YAML generation"""
response = self.client.get(
reverse("authentik_api:schema-json", kwargs={"format": ".yaml"}),
)
self.assertTrue(safe_load(response.content.decode()))
def test_json(self):
"""Test JSON generation"""
response = self.client.get(
reverse("authentik_api:schema-json", kwargs={"format": ".json"}),
)
self.assertTrue(loads(response.content.decode()))
def test_browser(self):
"""Test API Browser"""
response = self.client.get(
reverse("authentik_api:swagger"),
)
self.assertEqual(response.status_code, 200)

View File

@ -1,50 +1,70 @@
"""core Configs API"""
from drf_yasg.utils import swagger_auto_schema
from rest_framework.fields import BooleanField, CharField, ListField
from os import environ, path
from django.conf import settings
from django.db import models
from drf_spectacular.utils import extend_schema
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
from rest_framework.fields import BooleanField, CharField, ChoiceField, ListField
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
from rest_framework.views import APIView
from authentik.core.api.utils import PassiveSerializer
from authentik.events.geo import GEOIP_READER
from authentik.lib.config import CONFIG
class FooterLinkSerializer(PassiveSerializer):
"""Links returned in Config API"""
class Capabilities(models.TextChoices):
"""Define capabilities which influence which APIs can/should be used"""
href = CharField(read_only=True)
name = CharField(read_only=True)
CAN_SAVE_MEDIA = "can_save_media"
CAN_GEO_IP = "can_geo_ip"
CAN_BACKUP = "can_backup"
class ConfigSerializer(PassiveSerializer):
"""Serialize authentik Config into DRF Object"""
branding_logo = CharField(read_only=True)
branding_title = CharField(read_only=True)
ui_footer_links = ListField(child=FooterLinkSerializer(), read_only=True)
error_reporting_enabled = BooleanField(read_only=True)
error_reporting_environment = CharField(read_only=True)
error_reporting_send_pii = BooleanField(read_only=True)
capabilities = ListField(child=ChoiceField(choices=Capabilities.choices))
class ConfigsViewSet(ViewSet):
class ConfigView(APIView):
"""Read-only view set that returns the current session's Configs"""
permission_classes = [AllowAny]
@swagger_auto_schema(responses={200: ConfigSerializer(many=False)})
def list(self, request: Request) -> Response:
def get_capabilities(self) -> list[Capabilities]:
"""Get all capabilities this server instance supports"""
caps = []
deb_test = settings.DEBUG or settings.TEST
if path.ismount(settings.MEDIA_ROOT) or deb_test:
caps.append(Capabilities.CAN_SAVE_MEDIA)
if GEOIP_READER.enabled:
caps.append(Capabilities.CAN_GEO_IP)
if SERVICE_HOST_ENV_NAME in environ:
# Running in k8s, only s3 backup is supported
if CONFIG.y("postgresql.s3_backup"):
caps.append(Capabilities.CAN_BACKUP)
else:
# Running in compose, backup is always supported
caps.append(Capabilities.CAN_BACKUP)
return caps
@extend_schema(responses={200: ConfigSerializer(many=False)})
def get(self, request: Request) -> Response:
"""Retrive public configuration options"""
config = ConfigSerializer(
{
"branding_logo": CONFIG.y("authentik.branding.logo"),
"branding_title": CONFIG.y("authentik.branding.title"),
"error_reporting_enabled": CONFIG.y("error_reporting.enabled"),
"error_reporting_environment": CONFIG.y("error_reporting.environment"),
"error_reporting_send_pii": CONFIG.y("error_reporting.send_pii"),
"ui_footer_links": CONFIG.y("authentik.footer_links"),
"capabilities": self.get_capabilities(),
}
)
return Response(config.data)

View File

@ -1,18 +1,18 @@
"""api v2 urls"""
from django.urls import path, re_path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from django.urls import path
from drf_spectacular.views import SpectacularAPIView
from rest_framework import routers
from rest_framework.permissions import AllowAny
from authentik.admin.api.meta import AppsViewSet
from authentik.admin.api.metrics import AdministrationMetricsViewSet
from authentik.admin.api.system import SystemView
from authentik.admin.api.tasks import TaskViewSet
from authentik.admin.api.version import VersionViewSet
from authentik.admin.api.workers import WorkerViewSet
from authentik.api.v2.config import ConfigsViewSet
from authentik.api.views import SwaggerView
from authentik.admin.api.version import VersionView
from authentik.admin.api.workers import WorkerView
from authentik.api.v2.config import ConfigView
from authentik.api.views import APIBrowserView
from authentik.core.api.applications import ApplicationViewSet
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
from authentik.core.api.groups import GroupViewSet
from authentik.core.api.propertymappings import PropertyMappingViewSet
from authentik.core.api.providers import ProviderViewSet
@ -28,12 +28,12 @@ from authentik.flows.api.bindings import FlowStageBindingViewSet
from authentik.flows.api.flows import FlowViewSet
from authentik.flows.api.stages import StageViewSet
from authentik.flows.views import FlowExecutorView
from authentik.outposts.api.outpost_service_connections import (
from authentik.outposts.api.outposts import OutpostViewSet
from authentik.outposts.api.service_connections import (
DockerServiceConnectionViewSet,
KubernetesServiceConnectionViewSet,
ServiceConnectionViewSet,
)
from authentik.outposts.api.outposts import OutpostViewSet
from authentik.policies.api.bindings import PolicyBindingViewSet
from authentik.policies.api.policies import PolicyViewSet
from authentik.policies.dummy.api import DummyPolicyViewSet
@ -66,6 +66,11 @@ from authentik.sources.oauth.api.source_connection import (
)
from authentik.sources.plex.api import PlexSourceViewSet
from authentik.sources.saml.api import SAMLSourceViewSet
from authentik.stages.authenticator_duo.api import (
AuthenticatorDuoStageViewSet,
DuoAdminDeviceViewSet,
DuoDeviceViewSet,
)
from authentik.stages.authenticator_static.api import (
AuthenticatorStaticStageViewSet,
StaticAdminDeviceViewSet,
@ -97,24 +102,21 @@ from authentik.stages.user_delete.api import UserDeleteStageViewSet
from authentik.stages.user_login.api import UserLoginStageViewSet
from authentik.stages.user_logout.api import UserLogoutStageViewSet
from authentik.stages.user_write.api import UserWriteStageViewSet
from authentik.tenants.api import TenantViewSet
router = routers.DefaultRouter()
router.register("root/config", ConfigsViewSet, basename="configs")
router.register("admin/version", VersionViewSet, basename="admin_version")
router.register("admin/workers", WorkerViewSet, basename="admin_workers")
router.register("admin/metrics", AdministrationMetricsViewSet, basename="admin_metrics")
router.register("admin/system_tasks", TaskViewSet, basename="admin_system_tasks")
router.register("admin/apps", AppsViewSet, basename="apps")
router.register("core/authenticated_sessions", AuthenticatedSessionViewSet)
router.register("core/applications", ApplicationViewSet)
router.register("core/groups", GroupViewSet)
router.register("core/users", UserViewSet)
router.register("core/user_consent", UserConsentViewSet)
router.register("core/tokens", TokenViewSet)
router.register("core/tenants", TenantViewSet)
router.register("outposts/outposts", OutpostViewSet)
router.register("outposts/instances", OutpostViewSet)
router.register("outposts/service_connections/all", ServiceConnectionViewSet)
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
@ -166,14 +168,31 @@ router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
router.register("propertymappings/scope", ScopeMappingViewSet)
router.register("authenticators/duo", DuoDeviceViewSet)
router.register("authenticators/static", StaticDeviceViewSet)
router.register("authenticators/totp", TOTPDeviceViewSet)
router.register("authenticators/webauthn", WebAuthnDeviceViewSet)
router.register("authenticators/admin/static", StaticAdminDeviceViewSet)
router.register("authenticators/admin/totp", TOTPAdminDeviceViewSet)
router.register("authenticators/admin/webauthn", WebAuthnAdminDeviceViewSet)
router.register(
"authenticators/admin/duo",
DuoAdminDeviceViewSet,
basename="admin-duodevice",
)
router.register(
"authenticators/admin/static",
StaticAdminDeviceViewSet,
basename="admin-staticdevice",
)
router.register(
"authenticators/admin/totp", TOTPAdminDeviceViewSet, basename="admin-totpdevice"
)
router.register(
"authenticators/admin/webauthn",
WebAuthnAdminDeviceViewSet,
basename="admin-webauthndevice",
)
router.register("stages/all", StageViewSet)
router.register("stages/authenticator/duo", AuthenticatorDuoStageViewSet)
router.register("stages/authenticator/static", AuthenticatorStaticStageViewSet)
router.register("stages/authenticator/totp", AuthenticatorTOTPStageViewSet)
router.register("stages/authenticator/validate", AuthenticatorValidateStageViewSet)
@ -196,32 +215,26 @@ router.register("stages/user_write", UserWriteStageViewSet)
router.register("stages/dummy", DummyStageViewSet)
router.register("policies/dummy", DummyPolicyViewSet)
info = openapi.Info(
title="authentik API",
default_version="v2beta",
contact=openapi.Contact(email="hello@beryju.org"),
license=openapi.License(
name="GNU GPLv3",
url="https://github.com/goauthentik/authentik/blob/master/LICENSE",
),
)
SchemaView = get_schema_view(info, public=True, permission_classes=(AllowAny,))
urlpatterns = (
[
path("", SwaggerView.as_view(), name="swagger"),
path("", APIBrowserView.as_view(), name="schema-browser"),
]
+ router.urls
+ [
path(
"admin/metrics/",
AdministrationMetricsViewSet.as_view(),
name="admin_metrics",
),
path("admin/version/", VersionView.as_view(), name="admin_version"),
path("admin/workers/", WorkerView.as_view(), name="admin_workers"),
path("admin/system/", SystemView.as_view(), name="admin_system"),
path("root/config/", ConfigView.as_view(), name="config"),
path(
"flows/executor/<slug:flow_slug>/",
FlowExecutorView.as_view(),
name="flow-executor",
),
re_path(
r"^swagger(?P<format>\.json|\.yaml)$",
SchemaView.without_ui(cache_timeout=0),
name="schema-json",
),
path("schema/", SpectacularAPIView.as_view(), name="schema"),
]
)

View File

@ -5,18 +5,15 @@ from django.urls import reverse
from django.views.generic import TemplateView
class SwaggerView(TemplateView):
"""Show swagger view based on rapi-doc"""
class APIBrowserView(TemplateView):
"""Show browser view based on rapi-doc"""
template_name = "api/swagger.html"
template_name = "api/browser.html"
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
path = self.request.build_absolute_uri(
reverse(
"authentik_api:schema-json",
kwargs={
"format": ".json",
},
"authentik_api:schema",
)
)
return super().get_context_data(path=path, **kwargs)

View File

@ -1,14 +1,23 @@
"""Application API Views"""
from typing import Optional
from django.core.cache import cache
from django.db.models import QuerySet
from django.http.response import HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from drf_yasg import openapi
from drf_yasg.utils import no_body, swagger_auto_schema
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter,
OpenApiResponse,
extend_schema,
inline_serializer,
)
from rest_framework.decorators import action
from rest_framework.fields import SerializerMethodField
from rest_framework.fields import (
BooleanField,
CharField,
FileField,
IntegerField,
ReadOnlyField,
)
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
@ -20,9 +29,13 @@ from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.api.decorators import permission_required
from authentik.core.api.providers import ProviderSerializer
from authentik.core.models import Application
from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import Application, User
from authentik.events.models import EventAction
from authentik.policies.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult
from authentik.stages.user_login.stage import USER_LOGIN_AUTHENTICATED
LOGGER = get_logger()
@ -35,12 +48,10 @@ def user_app_cache_key(user_pk: str) -> str:
class ApplicationSerializer(ModelSerializer):
"""Application Serializer"""
launch_url = SerializerMethodField()
launch_url = ReadOnlyField(source="get_launch_url")
provider_obj = ProviderSerializer(source="get_provider", required=False)
def get_launch_url(self, instance: Application) -> Optional[str]:
"""Get generated launch URL"""
return instance.get_launch_url()
meta_icon = ReadOnlyField(source="get_meta_icon")
class Meta:
@ -58,9 +69,12 @@ class ApplicationSerializer(ModelSerializer):
"meta_publisher",
"policy_engine_mode",
]
extra_kwargs = {
"meta_icon": {"read_only": True},
}
class ApplicationViewSet(ModelViewSet):
class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Application Viewset"""
queryset = Application.objects.all()
@ -92,36 +106,48 @@ class ApplicationViewSet(ModelViewSet):
applications.append(application)
return applications
@swagger_auto_schema(
@extend_schema(
request=inline_serializer(
"CheckAccessRequest", fields={"for_user": IntegerField(required=False)}
),
responses={
204: "Access granted",
403: "Access denied",
}
200: PolicyTestResultSerializer(),
404: OpenApiResponse(description="for_user user not found"),
},
)
@action(detail=True, methods=["GET"])
@action(detail=True, methods=["POST"])
# pylint: disable=unused-argument
def check_access(self, request: Request, slug: str) -> Response:
"""Check access to a single application by slug"""
# Don't use self.get_object as that checks for view_application permission
# which the user might not have, even if they have access
application = get_object_or_404(Application, slug=slug)
engine = PolicyEngine(application, self.request.user, self.request)
# If the current user is superuser, they can set `for_user`
for_user = self.request.user
if self.request.user.is_superuser and "for_user" in request.data:
for_user = get_object_or_404(User, pk=request.data.get("for_user"))
engine = PolicyEngine(application, for_user, self.request)
engine.build()
if engine.passing:
return Response(status=204)
return Response(status=403)
result = engine.result
response = PolicyTestResultSerializer(PolicyResult(False))
if result.passing:
response = PolicyTestResultSerializer(PolicyResult(True))
if self.request.user.is_superuser:
response = PolicyTestResultSerializer(result)
return Response(response.data)
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(
@extend_schema(
parameters=[
OpenApiParameter(
name="superuser_full_list",
in_=openapi.IN_QUERY,
type=openapi.TYPE_BOOLEAN,
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
)
]
)
def list(self, request: Request) -> Response:
"""Custom list method that checks Policy based access instead of guardian"""
self.request.session.pop(USER_LOGIN_AUTHENTICATED, None)
queryset = self._filter_queryset_for_list(self.get_queryset())
self.paginate_queryset(queryset)
@ -151,17 +177,20 @@ class ApplicationViewSet(ModelViewSet):
return self.get_paginated_response(serializer.data)
@permission_required("authentik_core.change_application")
@swagger_auto_schema(
request_body=no_body,
manual_parameters=[
openapi.Parameter(
name="file",
in_=openapi.IN_FORM,
type=openapi.TYPE_FILE,
required=True,
@extend_schema(
request={
"multipart/form-data": inline_serializer(
"SetIcon",
fields={
"file": FileField(required=False),
"clear": BooleanField(default=False),
},
)
],
responses={200: "Success", 400: "Bad request"},
},
responses={
200: OpenApiResponse(description="Success"),
400: OpenApiResponse(description="Bad request"),
},
)
@action(
detail=True,
@ -175,16 +204,46 @@ class ApplicationViewSet(ModelViewSet):
"""Set application icon"""
app: Application = self.get_object()
icon = request.FILES.get("file", None)
if not icon:
clear = request.data.get("clear", False)
if clear:
# .delete() saves the model by default
app.meta_icon.delete()
return Response({})
if icon:
app.meta_icon = icon
app.save()
return Response({})
return HttpResponseBadRequest()
@permission_required("authentik_core.change_application")
@extend_schema(
request=inline_serializer("SetIconURL", fields={"url": CharField()}),
responses={
200: OpenApiResponse(description="Success"),
400: OpenApiResponse(description="Bad request"),
},
)
@action(
detail=True,
pagination_class=None,
filter_backends=[],
methods=["POST"],
)
# pylint: disable=unused-argument
def set_icon_url(self, request: Request, slug: str):
"""Set application icon (as URL)"""
app: Application = self.get_object()
url = request.data.get("url", None)
if url is None:
return HttpResponseBadRequest()
app.meta_icon = icon
app.meta_icon.name = url
app.save()
return Response({})
@permission_required(
"authentik_core.view_application", ["authentik_events.view_event"]
)
@swagger_auto_schema(responses={200: CoordinateSerializer(many=True)})
@extend_schema(responses={200: CoordinateSerializer(many=True)})
@action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=unused-argument
def metrics(self, request: Request, slug: str):

View File

@ -0,0 +1,117 @@
"""AuthenticatedSessions API Viewset"""
from typing import Optional, TypedDict
from django_filters.rest_framework import DjangoFilterBackend
from guardian.utils import get_anonymous_user
from rest_framework import mixins
from rest_framework.fields import SerializerMethodField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from ua_parser import user_agent_parser
from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import AuthenticatedSession
from authentik.events.geo import GEOIP_READER, GeoIPDict
class UserAgentDeviceDict(TypedDict):
"""User agent device"""
brand: str
family: str
model: str
class UserAgentOSDict(TypedDict):
"""User agent os"""
family: str
major: str
minor: str
patch: str
patch_minor: str
class UserAgentBrowserDict(TypedDict):
"""User agent browser"""
family: str
major: str
minor: str
patch: str
class UserAgentDict(TypedDict):
"""User agent details"""
device: UserAgentDeviceDict
os: UserAgentOSDict
user_agent: UserAgentBrowserDict
string: str
class AuthenticatedSessionSerializer(ModelSerializer):
"""AuthenticatedSession Serializer"""
current = SerializerMethodField()
user_agent = SerializerMethodField()
geo_ip = SerializerMethodField()
def get_current(self, instance: AuthenticatedSession) -> bool:
"""Check if session is currently active session"""
request: Request = self.context["request"]
return request._request.session.session_key == instance.session_key
def get_user_agent(self, instance: AuthenticatedSession) -> UserAgentDict:
"""Get parsed user agent"""
return user_agent_parser.Parse(instance.last_user_agent)
def get_geo_ip(
self, instance: AuthenticatedSession
) -> Optional[GeoIPDict]: # pragma: no cover
"""Get parsed user agent"""
return GEOIP_READER.city_dict(instance.last_ip)
class Meta:
model = AuthenticatedSession
fields = [
"uuid",
"current",
"user_agent",
"geo_ip",
"user",
"last_ip",
"last_user_agent",
"last_used",
"expires",
]
class AuthenticatedSessionViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""AuthenticatedSession Viewset"""
queryset = AuthenticatedSession.objects.all()
serializer_class = AuthenticatedSessionSerializer
search_fields = ["user__username", "last_ip", "last_user_agent"]
filterset_fields = ["user__username", "last_ip", "last_user_agent"]
ordering = ["user__username"]
filter_backends = [
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)

View File

@ -5,6 +5,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import is_dict
from authentik.core.models import Group
@ -20,7 +21,7 @@ class GroupSerializer(ModelSerializer):
fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"]
class GroupViewSet(ModelViewSet):
class GroupViewSet(UsedByMixin, ModelViewSet):
"""Group Viewset"""
queryset = Group.objects.all()

View File

@ -1,8 +1,8 @@
"""PropertyMapping API Views"""
from json import dumps
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
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 import mixins
from rest_framework.decorators import action
@ -14,6 +14,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
MetaNameSerializer,
PassiveSerializer,
@ -65,6 +66,7 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
class PropertyMappingViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
@ -78,10 +80,10 @@ class PropertyMappingViewSet(
filterset_fields = {"managed": ["isnull"]}
ordering = ["name"]
def get_queryset(self):
def get_queryset(self): # pragma: no cover
return PropertyMapping.objects.select_subclasses()
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable property-mapping types"""
@ -100,14 +102,17 @@ class PropertyMappingViewSet(
return Response(TypeCreateSerializer(data, many=True).data)
@permission_required("authentik_core.view_propertymapping")
@swagger_auto_schema(
request_body=PolicyTestSerializer(),
responses={200: PropertyMappingTestResultSerializer, 400: "Invalid parameters"},
manual_parameters=[
openapi.Parameter(
@extend_schema(
request=PolicyTestSerializer(),
responses={
200: PropertyMappingTestResultSerializer,
400: OpenApiResponse(description="Invalid parameters"),
},
parameters=[
OpenApiParameter(
name="format_result",
in_=openapi.IN_QUERY,
type=openapi.TYPE_BOOLEAN,
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
)
],
)

View File

@ -1,6 +1,6 @@
"""Provider API Views"""
from django.utils.translation import gettext_lazy as _
from drf_yasg.utils import swagger_auto_schema
from drf_spectacular.utils import extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField
@ -9,6 +9,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.models import Provider
from authentik.lib.utils.reflection import all_subclasses
@ -22,7 +23,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
component = SerializerMethodField()
def get_component(self, obj: Provider): # pragma: no cover
def get_component(self, obj: Provider) -> str: # pragma: no cover
"""Get object component so that we know how to edit the object"""
# pyright: reportGeneralTypeIssues=false
if obj.__class__ == Provider:
@ -48,6 +49,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
class ProviderViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
@ -63,10 +65,10 @@ class ProviderViewSet(
"application__name",
]
def get_queryset(self):
def get_queryset(self): # pragma: no cover
return Provider.objects.select_subclasses()
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable provider types"""

View File

@ -1,7 +1,7 @@
"""Source API Views"""
from typing import Iterable
from drf_yasg.utils import swagger_auto_schema
from drf_spectacular.utils import extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.request import Request
@ -10,6 +10,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.models import Source
from authentik.core.types import UserSettingSerializer
@ -24,7 +25,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
component = SerializerMethodField()
def get_component(self, obj: Source):
def get_component(self, obj: Source) -> str:
"""Get object component so that we know how to edit the object"""
# pyright: reportGeneralTypeIssues=false
if obj.__class__ == Source:
@ -52,6 +53,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
class SourceViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
@ -61,10 +63,10 @@ class SourceViewSet(
serializer_class = SourceSerializer
lookup_field = "slug"
def get_queryset(self):
def get_queryset(self): # pragma: no cover
return Source.objects.select_subclasses()
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable source types"""
@ -87,7 +89,7 @@ class SourceViewSet(
)
return Response(TypeCreateSerializer(data, many=True).data)
@swagger_auto_schema(responses={200: UserSettingSerializer(many=True)})
@extend_schema(responses={200: UserSettingSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def user_settings(self, request: Request) -> Response:
"""Get all sources the user can configure"""

View File

@ -1,6 +1,6 @@
"""Tokens API Viewset"""
from django.http.response import Http404
from drf_yasg.utils import swagger_auto_schema
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.fields import CharField
from rest_framework.request import Request
@ -9,6 +9,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Token, TokenIntents
@ -43,7 +44,7 @@ class TokenViewSerializer(PassiveSerializer):
key = CharField(read_only=True)
class TokenViewSet(ModelViewSet):
class TokenViewSet(UsedByMixin, ModelViewSet):
"""Token Viewset"""
lookup_field = "identifier"
@ -67,10 +68,10 @@ class TokenViewSet(ModelViewSet):
serializer.save(user=self.request.user, intent=TokenIntents.INTENT_API)
@permission_required("authentik_core.view_token_key")
@swagger_auto_schema(
@extend_schema(
responses={
200: TokenViewSerializer(many=False),
404: "Token not found or expired",
404: OpenApiResponse(description="Token not found or expired"),
}
)
@action(detail=True, pagination_class=None, filter_backends=[])

View File

@ -0,0 +1,102 @@
"""used_by mixin"""
from enum import Enum
from inspect import getmembers
from django.db.models.base import Model
from django.db.models.deletion import SET_DEFAULT, SET_NULL
from django.db.models.manager import Manager
from drf_spectacular.utils import extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import CharField, ChoiceField
from rest_framework.request import Request
from rest_framework.response import Response
from authentik.core.api.utils import PassiveSerializer
class DeleteAction(Enum):
"""Which action a delete will have on a used object"""
CASCADE = "cascade"
CASCADE_MANY = "cascade_many"
SET_NULL = "set_null"
SET_DEFAULT = "set_default"
class UsedBySerializer(PassiveSerializer):
"""A list of all objects referencing the queried object"""
app = CharField()
model_name = CharField()
pk = CharField()
name = CharField()
action = ChoiceField(choices=[(x.name, x.name) for x in DeleteAction])
def get_delete_action(manager: Manager) -> str:
"""Get the delete action from the Foreign key, falls back to cascade"""
if hasattr(manager, "field"):
if manager.field.remote_field.on_delete.__name__ == SET_NULL.__name__:
return DeleteAction.SET_NULL.name
if manager.field.remote_field.on_delete.__name__ == SET_DEFAULT.__name__:
return DeleteAction.SET_DEFAULT.name
if hasattr(manager, "source_field"):
return DeleteAction.CASCADE_MANY.name
return DeleteAction.CASCADE.name
class UsedByMixin:
"""Mixin to add a used_by endpoint to return a list of all objects using this object"""
@extend_schema(
responses={200: UsedBySerializer(many=True)},
)
@action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=invalid-name, unused-argument, too-many-locals
def used_by(self, request: Request, *args, **kwargs) -> Response:
"""Get a list of all objects that use this object"""
# pyright: reportGeneralTypeIssues=false
model: Model = self.get_object()
used_by = []
shadows = []
for attr_name, manager in getmembers(model, lambda x: isinstance(x, Manager)):
if attr_name == "objects": # pragma: no cover
continue
manager: Manager
if manager.model._meta.abstract:
continue
app = manager.model._meta.app_label
model_name = manager.model._meta.model_name
delete_action = get_delete_action(manager)
# To make sure we only apply shadows when there are any objects,
# but so we only apply them once, have a simple flag for the first object
first_object = True
for obj in get_objects_for_user(
request.user, f"{app}.view_{model_name}", manager
).all():
# Only merge shadows on first object
if first_object:
shadows += getattr(
manager.model._meta, "authentik_used_by_shadows", []
)
first_object = False
serializer = UsedBySerializer(
data={
"app": app,
"model_name": model_name,
"pk": str(obj.pk),
"name": str(obj),
"action": delete_action,
}
)
serializer.is_valid()
used_by.append(serializer.data)
# Check the shadows map and remove anything that should be shadowed
for idx, user in enumerate(used_by):
full_model_name = f"{user['app']}.{user['model_name']}"
if full_model_name in shadows:
del used_by[idx]
return Response(used_by)

View File

@ -7,7 +7,7 @@ from django.urls import reverse_lazy
from django.utils.http import urlencode
from django_filters.filters import BooleanFilter, CharFilter
from django_filters.filterset import FilterSet
from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_field
from guardian.utils import get_anonymous_user
from rest_framework.decorators import action
from rest_framework.fields import CharField, JSONField, SerializerMethodField
@ -25,6 +25,7 @@ from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.api.decorators import permission_required
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
from authentik.core.middleware import (
SESSION_IMPERSONATE_ORIGINAL_USER,
@ -32,7 +33,7 @@ from authentik.core.middleware import (
)
from authentik.core.models import Token, TokenIntents, User
from authentik.events.models import EventAction
from authentik.flows.models import Flow, FlowDesignation
from authentik.tenants.models import Tenant
class UserSerializer(ModelSerializer):
@ -77,13 +78,13 @@ class UserMetricsSerializer(PassiveSerializer):
logins_failed_per_1h = SerializerMethodField()
authorizations_per_1h = SerializerMethodField()
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
@extend_schema_field(CoordinateSerializer(many=True))
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)
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
@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"]
@ -91,7 +92,7 @@ class UserMetricsSerializer(PassiveSerializer):
action=EventAction.LOGIN_FAILED, context__username=user.username
)
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
@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"]
@ -131,7 +132,7 @@ class UsersFilter(FilterSet):
fields = ["username", "name", "is_active", "is_superuser", "attributes"]
class UserViewSet(ModelViewSet):
class UserViewSet(UsedByMixin, ModelViewSet):
"""User Viewset"""
queryset = User.objects.none()
@ -139,10 +140,10 @@ class UserViewSet(ModelViewSet):
search_fields = ["username", "name", "is_active"]
filterset_class = UsersFilter
def get_queryset(self):
def get_queryset(self): # pragma: no cover
return User.objects.all().exclude(pk=get_anonymous_user().pk)
@swagger_auto_schema(responses={200: SessionUserSerializer(many=False)})
@extend_schema(responses={200: SessionUserSerializer(many=False)})
@action(detail=False, pagination_class=None, filter_backends=[])
# pylint: disable=invalid-name
def me(self, request: Request) -> Response:
@ -158,7 +159,7 @@ class UserViewSet(ModelViewSet):
return Response(serializer.data)
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
@swagger_auto_schema(responses={200: UserMetricsSerializer(many=False)})
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
@action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=invalid-name, unused-argument
def metrics(self, request: Request, pk: int) -> Response:
@ -169,15 +170,19 @@ class UserViewSet(ModelViewSet):
return Response(serializer.data)
@permission_required("authentik_core.reset_user_password")
@swagger_auto_schema(
responses={"200": LinkSerializer(many=False), "404": "No recovery flow found."},
@extend_schema(
responses={
"200": LinkSerializer(many=False),
"404": OpenApiResponse(description="No recovery flow found."),
},
)
@action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=invalid-name, unused-argument
def recovery(self, request: Request, pk: int) -> Response:
"""Create a temporary link that a user can use to recover their accounts"""
tenant: Tenant = request._request.tenant
# Check that there is a recovery flow, if not return an error
flow = Flow.with_policy(request, designation=FlowDesignation.RECOVERY)
flow = tenant.flow_recovery
if not flow:
raise Http404
user: User = self.get_object()
@ -188,7 +193,8 @@ class UserViewSet(ModelViewSet):
)
querystring = urlencode({"token": token.key})
link = request.build_absolute_uri(
reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}"
reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
+ f"?{querystring}"
)
return Response({"link": link})

View File

@ -28,6 +28,9 @@ class PassiveSerializer(Serializer):
) -> Model: # pragma: no cover
return Model()
class Meta:
model = Model
class MetaNameSerializer(PassiveSerializer):
"""Add verbose names to response"""

View File

@ -2,6 +2,10 @@
from importlib import import_module
from django.apps import AppConfig
from django.db import ProgrammingError
from authentik.core.signals import GAUGE_MODELS
from authentik.lib.utils.reflection import get_apps
class AuthentikCoreConfig(AppConfig):
@ -15,3 +19,12 @@ class AuthentikCoreConfig(AppConfig):
def ready(self):
import_module("authentik.core.signals")
import_module("authentik.core.managed")
try:
for app in get_apps():
for model in app.get_models():
GAUGE_MODELS.labels(
model_name=model._meta.model_name,
app=model._meta.app_label,
).set(model.objects.count())
except ProgrammingError:
pass

View File

@ -4,7 +4,7 @@ from channels.generic.websocket import JsonWebsocketConsumer
from rest_framework.exceptions import AuthenticationFailed
from structlog.stdlib import get_logger
from authentik.api.auth import token_from_header
from authentik.api.authentication import token_from_header
from authentik.core.models import User
LOGGER = get_logger()

View File

@ -42,10 +42,14 @@ class RequestIDMiddleware:
if not hasattr(request, "request_id"):
request_id = uuid4().hex
setattr(request, "request_id", request_id)
LOCAL.authentik = {"request_id": request_id}
LOCAL.authentik = {
"request_id": request_id,
"host": request.get_host(),
}
response = self.get_response(request)
response[RESPONSE_HEADER_ID] = request.request_id
del LOCAL.authentik["request_id"]
del LOCAL.authentik["host"]
return response
@ -54,4 +58,5 @@ def structlog_add_request_id(logger: Logger, method_name: str, event_dict):
"""If threadlocal has authentik defined, add request_id to log"""
if hasattr(LOCAL, "authentik"):
event_dict["request_id"] = LOCAL.authentik.get("request_id", "")
event_dict["host"] = LOCAL.authentik.get("host", "")
return event_dict

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.3 on 2021-05-14 08:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0020_source_user_matching_mode"),
]
operations = [
migrations.AlterField(
model_name="application",
name="slug",
field=models.SlugField(
help_text="Internal application name, used in URLs.", unique=True
),
),
]

View File

@ -0,0 +1,63 @@
# Generated by Django 3.2.3 on 2021-05-29 22:14
import uuid
import django.db.models.deletion
from django.apps.registry import Apps
from django.conf import settings
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
import authentik.core.models
def migrate_sessions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
session_keys = cache.keys(KEY_PREFIX + "*")
cache.delete_many(session_keys)
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0021_alter_application_slug"),
]
operations = [
migrations.CreateModel(
name="AuthenticatedSession",
fields=[
(
"expires",
models.DateTimeField(
default=authentik.core.models.default_token_duration
),
),
("expiring", models.BooleanField(default=True)),
(
"uuid",
models.UUIDField(
default=uuid.uuid4, primary_key=True, serialize=False
),
),
("session_key", models.CharField(max_length=40)),
("last_ip", models.TextField()),
("last_user_agent", models.TextField(blank=True)),
("last_used", models.DateTimeField(auto_now=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
migrations.RunPython(migrate_sessions),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.3 on 2021-06-02 21:51
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0022_authenticatedsession"),
]
operations = [
migrations.AlterField(
model_name="application",
name="meta_launch_url",
field=models.TextField(
blank=True,
default="",
validators=[django.core.validators.URLValidator()],
),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 3.2.3 on 2021-06-03 09:33
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.models import Count
def fix_duplicates(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
Token = apps.get_model("authentik_core", "token")
identifiers = (
Token.objects.using(db_alias)
.values("identifier")
.annotate(identifier_count=Count("identifier"))
.filter(identifier_count__gt=1)
)
for ident in identifiers:
Token.objects.using(db_alias).filter(identifier=ident["identifier"]).delete()
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0023_alter_application_meta_launch_url"),
]
operations = [
migrations.RunPython(fix_duplicates),
migrations.AlterField(
model_name="token",
name="identifier",
field=models.SlugField(max_length=255, unique=True),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.3 on 2021-06-05 19:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0024_alter_token_identifier"),
]
operations = [
migrations.AlterField(
model_name="application",
name="meta_icon",
field=models.FileField(
default=None, null=True, upload_to="application-icons/"
),
),
]

View File

@ -5,9 +5,11 @@ from typing import Any, Optional, Type
from urllib.parse import urlencode
from uuid import uuid4
import django.db.models.options as options
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager
from django.core import validators
from django.db import models
from django.db.models import Q, QuerySet
from django.http import HttpRequest
@ -23,11 +25,11 @@ 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
from authentik.flows.challenge import Challenge
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.flows.models import Flow
from authentik.lib.config import CONFIG
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
from authentik.lib.utils.http import get_client_ip
from authentik.managed.models import ManagedModel
from authentik.policies.models import PolicyBindingModel
@ -40,6 +42,9 @@ GRAVATAR_URL = "https://secure.gravatar.com"
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
options.DEFAULT_NAMES = options.DEFAULT_NAMES + ("authentik_used_by_shadows",)
def default_token_duration():
"""Default duration a Token is valid"""
return now() + timedelta(minutes=30)
@ -207,17 +212,35 @@ class Application(PolicyBindingModel):
add custom fields and other properties"""
name = models.TextField(help_text=_("Application's display Name."))
slug = models.SlugField(help_text=_("Internal application name, used in URLs."))
slug = models.SlugField(
help_text=_("Internal application name, used in URLs."), unique=True
)
provider = models.OneToOneField(
"Provider", null=True, blank=True, default=None, on_delete=models.SET_DEFAULT
)
meta_launch_url = models.URLField(default="", blank=True)
meta_launch_url = models.TextField(
default="", blank=True, validators=[validators.URLValidator()]
)
# For template applications, this can be set to /static/authentik/applications/*
meta_icon = models.FileField(upload_to="application-icons/", default="", blank=True)
meta_icon = models.FileField(
upload_to="application-icons/", default=None, null=True
)
meta_description = models.TextField(default="", blank=True)
meta_publisher = models.TextField(default="", blank=True)
@property
def get_meta_icon(self) -> Optional[str]:
"""Get the URL to the App Icon image. If the name is /static or starts with http
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"
):
return self.meta_icon.name
return self.meta_icon.url
def get_launch_url(self) -> Optional[str]:
"""Get launch URL if set, otherwise attempt to get launch URL based on provider."""
if self.meta_launch_url:
@ -322,9 +345,9 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
return None
@property
def ui_user_settings(self) -> Optional[Challenge]:
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."""
user settings are available, or UserSettingSerializer."""
return None
def __str__(self):
@ -386,7 +409,7 @@ class Token(ManagedModel, ExpiringModel):
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""
token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
identifier = models.SlugField(max_length=255)
identifier = models.SlugField(max_length=255, unique=True)
key = models.TextField(default=default_token_key)
intent = models.TextField(
choices=TokenIntents.choices, default=TokenIntents.INTENT_VERIFICATION
@ -450,3 +473,33 @@ class PropertyMapping(SerializerModel, ManagedModel):
verbose_name = _("Property Mapping")
verbose_name_plural = _("Property Mappings")
class AuthenticatedSession(ExpiringModel):
"""Additional session class for authenticated users. Augments the standard django session
to achieve the following:
- Make it queryable by user
- Have a direct connection to user objects
- Allow users to view their own sessions and terminate them
- Save structured and well-defined information.
"""
uuid = models.UUIDField(default=uuid4, primary_key=True)
session_key = models.CharField(max_length=40)
user = models.ForeignKey(User, on_delete=models.CASCADE)
last_ip = models.TextField()
last_user_agent = models.TextField(blank=True)
last_used = models.DateTimeField(auto_now=True)
@staticmethod
def from_request(request: HttpRequest, user: User) -> "AuthenticatedSession":
"""Create a new session from a http request"""
return AuthenticatedSession(
session_key=request.session.session_key,
user=user,
last_ip=get_client_ip(request),
last_user_agent=request.META.get("HTTP_USER_AGENT", ""),
expires=request.session.get_expiry_date(),
)

View File

@ -1,20 +1,38 @@
"""authentik core signals"""
from typing import TYPE_CHECKING
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.core.cache import cache
from django.core.signals import Signal
from django.db.models import Model
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.http.request import HttpRequest
from prometheus_client import Gauge
# Arguments: user: User, password: str
password_changed = Signal()
GAUGE_MODELS = Gauge(
"authentik_models", "Count of various objects", ["model_name", "app"]
)
if TYPE_CHECKING:
from authentik.core.models import User
@receiver(post_save)
# pylint: disable=unused-argument
def post_save_application(sender, instance, created: bool, **_):
def post_save_application(sender: type[Model], instance, created: bool, **_):
"""Clear user's application cache upon application creation"""
from authentik.core.api.applications import user_app_cache_key
from authentik.core.models import Application
GAUGE_MODELS.labels(
model_name=sender._meta.model_name,
app=sender._meta.app_label,
).set(sender.objects.count())
if sender != Application:
return
if not created: # pragma: no cover
@ -22,3 +40,23 @@ def post_save_application(sender, instance, created: bool, **_):
# Also delete user application cache
keys = cache.keys(user_app_cache_key("*"))
cache.delete_many(keys)
@receiver(user_logged_in)
# pylint: disable=unused-argument
def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
"""Create an AuthenticatedSession from request"""
from authentik.core.models import AuthenticatedSession
AuthenticatedSession.from_request(request, user).save()
@receiver(user_logged_out)
# pylint: disable=unused-argument
def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
"""Delete AuthenticatedSession if it exists"""
from authentik.core.models import AuthenticatedSession
AuthenticatedSession.objects.filter(
session_key=request.session.session_key
).delete()

View File

@ -7,8 +7,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}{% trans title|default:config.authentik.branding.title %}{% endblock %}</title>
<link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
<title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title>
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}?v={{ ak_version }}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}?v={{ ak_version }}">

View File

@ -14,7 +14,7 @@
{% endblock %}
{% block title %}
{% trans 'End session' %} - {{ config.authentik.branding.title }}
{% trans 'End session' %} - {{ tenant.branding_title }}
{% endblock %}
{% block card_title %}

View File

@ -4,7 +4,9 @@
{% load i18n %}
{% block head_before %}
{% if flow.compatibility_mode %}
<script>ShadyDOM = { force: !navigator.webdriver };</script>
{% endif %}
{% endblock %}
{% block head %}

View File

@ -26,10 +26,7 @@
<div class="ak-login-container">
<header class="pf-c-login__header">
<div class="pf-c-brand ak-brand">
<img src="{{ config.authentik.branding.logo }}" alt="authentik icon" />
{% if config.authentik.branding.title_show %}
<p>{{ config.authentik.branding.title }}</p>
{% endif %}
<img src="{{ tenant.branding_logo }}" alt="authentik icon" />
</div>
</header>
{% block main_container %}
@ -49,12 +46,12 @@
<footer class="pf-c-login__footer">
<p></p>
<ul class="pf-c-list pf-m-inline">
{% for link in config.authentik.footer_links %}
{% for link in footer_links %}
<li>
<a href="{{ link.href }}">{{ link.name }}</a>
</li>
{% endfor %}
{% if config.authentik.branding.title != "authentik" %}
{% if tenant.branding_title != "authentik" %}
<li>
<a href="https://goauthentik.io">
{% trans 'Powered by authentik' %}

View File

@ -26,20 +26,26 @@ class TestApplicationsAPI(APITestCase):
def test_check_access(self):
"""Test check_access operation"""
self.client.force_login(self.user)
response = self.client.get(
response = self.client.post(
reverse(
"authentik_api:application-check-access",
kwargs={"slug": self.allowed.slug},
)
)
self.assertEqual(response.status_code, 204)
response = self.client.get(
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content), {"messages": [], "passing": True}
)
response = self.client.post(
reverse(
"authentik_api:application-check-access",
kwargs={"slug": self.denied.slug},
)
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content), {"messages": ["dummy"], "passing": False}
)
def test_list(self):
"""Test list operation without superuser_full_list"""

View File

@ -0,0 +1,31 @@
"""Test AuthenticatedSessions API"""
from json import loads
from django.urls.base import reverse
from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import User
class TestAuthenticatedSessionsAPI(APITestCase):
"""Test AuthenticatedSessions API"""
def setUp(self) -> None:
super().setUp()
self.user = User.objects.get(username="akadmin")
self.other_user = User.objects.create(username="normal-user")
def test_list(self):
"""Test session list endpoint"""
self.client.force_login(self.user)
response = self.client.get(reverse("authentik_api:authenticatedsession-list"))
self.assertEqual(response.status_code, 200)
def test_non_admin_list(self):
"""Test non-admin list"""
self.client.force_login(self.other_user)
response = self.client.get(reverse("authentik_api:authenticatedsession-list"))
self.assertEqual(response.status_code, 200)
body = loads(force_str(response.content))
self.assertEqual(body["pagination"]["count"], 1)

View File

@ -0,0 +1,29 @@
"""Test Users API"""
from django.urls.base import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User
class TestUsersAPI(APITestCase):
"""Test Users API"""
def setUp(self) -> None:
self.admin = User.objects.get(username="akadmin")
self.user = User.objects.create(username="test-user")
def test_metrics(self):
"""Test user's metrics"""
self.client.force_login(self.admin)
response = self.client.get(
reverse("authentik_api:user-metrics", kwargs={"pk": self.user.pk})
)
self.assertEqual(response.status_code, 200)
def test_metrics_denied(self):
"""Test user's metrics (non-superuser)"""
self.client.force_login(self.user)
response = self.client.get(
reverse("authentik_api:user-metrics", kwargs={"pk": self.user.pk})
)
self.assertEqual(response.status_code, 403)

View File

@ -36,3 +36,4 @@ class UserSettingSerializer(PassiveSerializer):
object_uid = CharField()
component = CharField()
title = CharField()
configure_url = CharField()

View File

@ -6,6 +6,8 @@ from django.views.generic import RedirectView
from django.views.generic.base import TemplateView
from authentik.core.views import impersonate
from authentik.core.views.interface import FlowInterfaceView
from authentik.core.views.session import EndSessionView
urlpatterns = [
path(
@ -32,7 +34,18 @@ urlpatterns = [
),
path(
"if/flow/<slug:flow_slug>/",
ensure_csrf_cookie(TemplateView.as_view(template_name="if/flow.html")),
ensure_csrf_cookie(FlowInterfaceView.as_view()),
name="if-flow",
),
path(
"if/session-end/<slug:application_slug>/",
ensure_csrf_cookie(EndSessionView.as_view()),
name="if-session-end",
),
# Fallback for WS
path("ws/outpost/<uuid:pk>/", TemplateView.as_view(template_name="if/admin.html")),
path(
"ws/client/",
TemplateView.as_view(template_name="if/admin.html"),
),
]

View File

@ -0,0 +1,17 @@
"""Interface views"""
from typing import Any
from django.shortcuts import get_object_or_404
from django.views.generic.base import TemplateView
from authentik.flows.models import Flow
class FlowInterfaceView(TemplateView):
"""Flow interface"""
template_name = "if/flow.html"
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
return super().get_context_data(**kwargs)

View File

@ -1,22 +1,24 @@
"""authentik OAuth2 Session Views"""
"""authentik Session Views"""
from typing import Any
from django.shortcuts import get_object_or_404
from django.views.generic.base import TemplateView
from authentik.core.models import Application
from authentik.policies.views import PolicyAccessView
class EndSessionView(TemplateView):
class EndSessionView(TemplateView, PolicyAccessView):
"""Allow the client to end the Session"""
template_name = "providers/oauth2/end_session.html"
template_name = "if/end_session.html"
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
context["application"] = get_object_or_404(
def resolve_provider_application(self):
self.application = get_object_or_404(
Application, slug=self.kwargs["application_slug"]
)
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
context["application"] = self.application
return context

View File

@ -1,12 +1,14 @@
"""Crypto API Views"""
import django_filters
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import load_pem_x509_certificate
from django.http.response import HttpResponse
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from django_filters import FilterSet
from django_filters.filters import BooleanFilter
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.fields import (
CharField,
@ -20,6 +22,7 @@ from rest_framework.serializers import ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair
@ -33,6 +36,9 @@ class CertificateKeyPairSerializer(ModelSerializer):
cert_subject = SerializerMethodField()
private_key_available = SerializerMethodField()
certificate_download_url = SerializerMethodField()
private_key_download_url = SerializerMethodField()
def get_cert_subject(self, instance: CertificateKeyPair) -> str:
"""Get certificate subject as full rfc4514"""
return instance.certificate.subject.rfc4514_string()
@ -41,6 +47,26 @@ class CertificateKeyPairSerializer(ModelSerializer):
"""Show if this keypair has a private key configured or not"""
return instance.key_data != "" and instance.key_data is not None
def get_certificate_download_url(self, instance: CertificateKeyPair) -> str:
"""Get URL to download certificate"""
return (
reverse(
"authentik_api:certificatekeypair-view-certificate",
kwargs={"pk": instance.pk},
)
+ "?download"
)
def get_private_key_download_url(self, instance: CertificateKeyPair) -> str:
"""Get URL to download private key"""
return (
reverse(
"authentik_api:certificatekeypair-view-private-key",
kwargs={"pk": instance.pk},
)
+ "?download"
)
def validate_certificate_data(self, value: str) -> str:
"""Verify that input is a valid PEM x509 Certificate"""
try:
@ -77,6 +103,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
"cert_expiry",
"cert_subject",
"private_key_available",
"certificate_download_url",
"private_key_download_url",
]
extra_kwargs = {
"key_data": {"write_only": True},
@ -100,10 +128,10 @@ class CertificateGenerationSerializer(PassiveSerializer):
validity_days = IntegerField(initial=365)
class CertificateKeyPairFilter(django_filters.FilterSet):
class CertificateKeyPairFilter(FilterSet):
"""Filter for certificates"""
has_key = django_filters.BooleanFilter(
has_key = BooleanFilter(
label="Only return certificate-key pairs with keys", method="filter_has_key"
)
@ -117,7 +145,7 @@ class CertificateKeyPairFilter(django_filters.FilterSet):
fields = ["name"]
class CertificateKeyPairViewSet(ModelViewSet):
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
"""CertificateKeyPair Viewset"""
queryset = CertificateKeyPair.objects.all()
@ -125,9 +153,12 @@ class CertificateKeyPairViewSet(ModelViewSet):
filterset_class = CertificateKeyPairFilter
@permission_required(None, ["authentik_crypto.add_certificatekeypair"])
@swagger_auto_schema(
request_body=CertificateGenerationSerializer(),
responses={200: CertificateKeyPairSerializer, 400: "Bad request"},
@extend_schema(
request=CertificateGenerationSerializer(),
responses={
200: CertificateKeyPairSerializer,
400: OpenApiResponse(description="Bad request"),
},
)
@action(detail=False, methods=["POST"])
def generate(self, request: Request) -> Response:
@ -147,12 +178,12 @@ class CertificateKeyPairViewSet(ModelViewSet):
serializer = self.get_serializer(instance)
return Response(serializer.data)
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(
@extend_schema(
parameters=[
OpenApiParameter(
name="download",
in_=openapi.IN_QUERY,
type=openapi.TYPE_BOOLEAN,
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
)
],
responses={200: CertificateDataSerializer(many=False)},
@ -180,12 +211,12 @@ class CertificateKeyPairViewSet(ModelViewSet):
CertificateDataSerializer({"data": certificate.certificate_data}).data
)
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(
@extend_schema(
parameters=[
OpenApiParameter(
name="download",
in_=openapi.IN_QUERY,
type=openapi.TYPE_BOOLEAN,
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
)
],
responses={200: CertificateDataSerializer(many=False)},

View File

@ -4,10 +4,14 @@ import datetime
from django.test import TestCase
from django.urls import reverse
from authentik.core.api.used_by import DeleteAction
from authentik.core.models import User
from authentik.crypto.api import CertificateKeyPairSerializer
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.providers.oauth2.generators import generate_client_secret
from authentik.providers.oauth2.models import OAuth2Provider
class TestCrypto(TestCase):
@ -91,3 +95,35 @@ class TestCrypto(TestCase):
)
self.assertEqual(200, response.status_code)
self.assertIn("Content-Disposition", response)
def test_used_by(self):
"""Test used_by endpoint"""
self.client.force_login(User.objects.get(username="akadmin"))
keypair = CertificateKeyPair.objects.first()
provider = OAuth2Provider.objects.create(
name="test",
client_id="test",
client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(),
redirect_uris="http://localhost",
rsa_key=CertificateKeyPair.objects.first(),
)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-used-by",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(200, response.status_code)
self.assertJSONEqual(
response.content.decode(),
[
{
"app": "authentik_providers_oauth2",
"model_name": "oauth2provider",
"pk": str(provider.pk),
"name": str(provider),
"action": DeleteAction.SET_NULL.name,
}
],
)

View File

@ -2,7 +2,8 @@
import django_filters
from django.db.models.aggregates import Count
from django.db.models.fields.json import KeyTextTransform
from drf_yasg.utils import swagger_auto_schema
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import CharField, DictField, IntegerField
@ -38,12 +39,6 @@ class EventSerializer(ModelSerializer):
]
class EventTopPerUserParams(PassiveSerializer):
"""Query params for top_per_user"""
top_n = IntegerField(default=15)
class EventTopPerUserSerializer(PassiveSerializer):
"""Response object of Event's top_per_user"""
@ -111,12 +106,19 @@ class EventViewSet(ReadOnlyModelViewSet):
]
filterset_class = EventsFilter
@swagger_auto_schema(
method="GET",
@extend_schema(
methods=["GET"],
responses={200: EventTopPerUserSerializer(many=True)},
query_serializer=EventTopPerUserParams,
parameters=[
OpenApiParameter(
"top_n",
type=OpenApiTypes.INT,
location=OpenApiParameter.QUERY,
required=False,
)
],
)
@action(detail=False, methods=["GET"])
@action(detail=False, methods=["GET"], pagination_class=None)
def top_per_user(self, request: Request):
"""Get the top_n events grouped by user count"""
filtered_action = request.query_params.get("action", EventAction.LOGIN)
@ -134,7 +136,7 @@ class EventViewSet(ReadOnlyModelViewSet):
.order_by("-counted_events")[:top_n]
)
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def actions(self, request: Request) -> Response:
"""Get all actions"""

View File

@ -1,12 +1,13 @@
"""Notification API Views"""
from django_filters.rest_framework import DjangoFilterBackend
from guardian.utils import get_anonymous_user
from rest_framework import mixins
from rest_framework.fields import ReadOnlyField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.events.api.event import EventSerializer
from authentik.events.models import Notification
@ -35,6 +36,7 @@ class NotificationViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
@ -49,12 +51,5 @@ class NotificationViewSet(
"event",
"seen",
]
filter_backends = [
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
return Notification.objects.filter(user=user.pk)
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]

View File

@ -3,6 +3,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.events.models import NotificationRule
@ -24,7 +25,7 @@ class NotificationRuleSerializer(ModelSerializer):
]
class NotificationRuleViewSet(ModelViewSet):
class NotificationRuleViewSet(UsedByMixin, ModelViewSet):
"""NotificationRule Viewset"""
queryset = NotificationRule.objects.all()

View File

@ -1,5 +1,6 @@
"""NotificationTransport API Views"""
from drf_yasg.utils import no_body, swagger_auto_schema
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.request import Request
@ -8,6 +9,7 @@ from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.events.models import (
Notification,
NotificationSeverity,
@ -22,7 +24,7 @@ class NotificationTransportSerializer(ModelSerializer):
mode_verbose = SerializerMethodField()
def get_mode_verbose(self, instance: NotificationTransport):
def get_mode_verbose(self, instance: NotificationTransport) -> str:
"""Return selected mode with a UI Label"""
return TransportMode(instance.mode).label
@ -51,19 +53,19 @@ class NotificationTransportTestSerializer(Serializer):
raise NotImplementedError
class NotificationTransportViewSet(ModelViewSet):
class NotificationTransportViewSet(UsedByMixin, ModelViewSet):
"""NotificationTransport Viewset"""
queryset = NotificationTransport.objects.all()
serializer_class = NotificationTransportSerializer
@permission_required("authentik_events.change_notificationtransport")
@swagger_auto_schema(
@extend_schema(
responses={
200: NotificationTransportTestSerializer(many=False),
503: "Failed to test transport",
500: OpenApiResponse(description="Failed to test transport"),
},
request_body=no_body,
request=OpenApiTypes.NONE,
)
@action(detail=True, pagination_class=None, filter_backends=[], methods=["post"])
# pylint: disable=invalid-name, unused-argument
@ -83,4 +85,4 @@ class NotificationTransportViewSet(ModelViewSet):
response.is_valid()
return Response(response.data)
except NotificationTransportError as exc:
return Response(str(exc.__cause__ or None), status=503)
return Response(str(exc.__cause__ or None), status=500)

View File

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

View File

@ -1,7 +1,12 @@
"""events GeoIP Reader"""
from typing import Optional
from datetime import datetime
from os import stat
from time import time
from typing import Optional, TypedDict
from geoip2.database import Reader
from geoip2.errors import GeoIP2Error
from geoip2.models import City
from structlog.stdlib import get_logger
from authentik.lib.config import CONFIG
@ -9,17 +14,78 @@ from authentik.lib.config import CONFIG
LOGGER = get_logger()
def get_geoip_reader() -> Optional[Reader]:
"""Get GeoIP Reader, if configured, otherwise none"""
path = CONFIG.y("authentik.geoip")
if path == "" or not path:
return None
try:
reader = Reader(path)
LOGGER.info("Enabled GeoIP support")
return reader
except OSError:
return None
class GeoIPDict(TypedDict):
"""GeoIP Details"""
continent: str
country: str
lat: float
long: float
city: str
GEOIP_READER = get_geoip_reader()
class GeoIPReader:
"""Slim wrapper around GeoIP API"""
__reader: Optional[Reader] = None
__last_mtime: float = 0.0
def __init__(self):
self.__open()
def __open(self):
"""Get GeoIP Reader, if configured, otherwise none"""
path = CONFIG.y("authentik.geoip")
if path == "" or not path:
return
try:
reader = Reader(path)
LOGGER.info("Loaded GeoIP database")
self.__reader = reader
self.__last_mtime = stat(path).st_mtime
except OSError as exc:
LOGGER.warning("Failed to load GeoIP database", exc=exc)
def __check_expired(self):
"""Check if the geoip database has been opened longer than 8 hours,
and re-open it, as it will probably will have been re-downloaded"""
now = time()
diff = datetime.fromtimestamp(now) - datetime.fromtimestamp(self.__last_mtime)
diff_hours = diff.total_seconds() // 3600
if diff_hours >= 8:
LOGGER.info("GeoIP databased loaded too long, re-opening", diff=diff)
self.__open()
@property
def enabled(self) -> bool:
"""Check if GeoIP is enabled"""
return bool(self.__reader)
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
def city_dict(self, ip_address: str) -> Optional[GeoIPDict]:
"""Wrapper for self.city that returns a dict"""
city = self.city(ip_address)
if not city:
return None
city_dict: GeoIPDict = {
"continent": city.continent.code,
"country": city.country.iso_code,
"lat": city.location.latitude,
"long": city.location.longitude,
"city": "",
}
if city.city.name:
city_dict["city"] = city.city.name
return city_dict
GEOIP_READER = GeoIPReader()

View File

@ -0,0 +1,45 @@
# Generated by Django 3.2.3 on 2021-06-09 07:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0014_expiry"),
]
operations = [
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"),
]
),
),
]

View File

@ -10,7 +10,7 @@ from django.db import models
from django.http import HttpRequest
from django.utils.timezone import now
from django.utils.translation import gettext as _
from geoip2.errors import GeoIP2Error
from prometheus_client import Gauge
from requests import RequestException, post
from structlog.stdlib import get_logger
@ -28,6 +28,11 @@ from authentik.policies.models import PolicyBindingModel
from authentik.stages.email.utils import TemplateEmailMessage
LOGGER = get_logger("authentik.events")
GAUGE_EVENTS = Gauge(
"authentik_events",
"Events in authentik",
["action", "user_username", "app", "client_ip"],
)
def default_event_duration():
@ -72,6 +77,7 @@ class EventAction(models.TextChoices):
MODEL_CREATED = "model_created"
MODEL_UPDATED = "model_updated"
MODEL_DELETED = "model_deleted"
EMAIL_SENT = "email_sent"
UPDATE_AVAILABLE = "update_available"
@ -143,7 +149,7 @@ class Event(ExpiringModel):
request.session[SESSION_IMPERSONATE_USER]
)
# User 255.255.255.255 as fallback if IP cannot be determined
self.client_ip = get_client_ip(request) or "255.255.255.255"
self.client_ip = get_client_ip(request)
# Apply GeoIP Data, when enabled
self.with_geoip()
# If there's no app set, we get it from the requests too
@ -152,22 +158,20 @@ class Event(ExpiringModel):
self.save()
return self
def with_geoip(self):
def with_geoip(self): # pragma: no cover
"""Apply GeoIP Data, when enabled"""
if not GEOIP_READER:
city = GEOIP_READER.city_dict(self.client_ip)
if not city:
return
try:
response = GEOIP_READER.city(self.client_ip)
self.context["geo"] = {
"continent": response.continent.code,
"country": response.country.iso_code,
"lat": response.location.latitude,
"long": response.location.longitude,
}
if response.city.name:
self.context["geo"]["city"] = response.city.name
except GeoIP2Error as exc:
LOGGER.warning("Failed to add geoIP Data to event", exc=exc)
self.context["geo"] = city
def _set_prom_metrics(self):
GAUGE_EVENTS.labels(
action=self.action,
user_username=self.user.get("username"),
app=self.app,
client_ip=self.client_ip,
).set(self.created.timestamp())
def save(self, *args, **kwargs):
if self._state.adding:
@ -178,7 +182,8 @@ class Event(ExpiringModel):
client_ip=self.client_ip,
user=self.user,
)
return super().save(*args, **kwargs)
super().save(*args, **kwargs)
self._set_prom_metrics()
@property
def summary(self) -> str:

View File

@ -2,14 +2,22 @@
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from timeit import default_timer
from traceback import format_tb
from typing import Any, Optional
from celery import Task
from django.core.cache import cache
from prometheus_client import Gauge
from authentik.events.models import Event, EventAction
GAUGE_TASKS = Gauge(
"authentik_system_tasks",
"System tasks and their status",
["task_name", "task_uid", "status"],
)
class TaskResultStatus(Enum):
"""Possible states of tasks"""
@ -43,7 +51,9 @@ class TaskInfo:
"""Info about a task run"""
task_name: str
finish_timestamp: datetime
start_timestamp: float
finish_timestamp: float
finish_time: datetime
result: TaskResult
@ -73,12 +83,28 @@ class TaskInfo:
"""Delete task info from cache"""
return cache.delete(f"task_{self.task_name}")
def set_prom_metrics(self):
"""Update prometheus metrics"""
start = default_timer()
if hasattr(self, "start_timestamp"):
start = self.start_timestamp
try:
duration = max(self.finish_timestamp - start, 0)
except TypeError:
duration = 0
GAUGE_TASKS.labels(
task_name=self.task_name,
task_uid=self.result.uid or "",
status=self.result.status,
).set(duration)
def save(self, timeout_hours=6):
"""Save task into cache"""
key = f"task_{self.task_name}"
if self.result.uid:
key += f"_{self.result.uid}"
self.task_name += f"_{self.result.uid}"
self.set_prom_metrics()
cache.set(key, self, timeout=timeout_hours * 60 * 60)
@ -98,6 +124,7 @@ class MonitoredTask(Task):
self._uid = None
self._result = TaskResult(status=TaskResultStatus.ERROR, messages=[])
self.result_timeout_hours = 6
self.start = default_timer()
def set_uid(self, uid: str):
"""Set UID, so in the case of an unexpected error its saved correctly"""
@ -117,7 +144,9 @@ class MonitoredTask(Task):
TaskInfo(
task_name=self.__name__,
task_description=self.__doc__,
finish_timestamp=datetime.now(),
start_timestamp=self.start,
finish_timestamp=default_timer(),
finish_time=datetime.now(),
result=self._result,
task_call_module=self.__module__,
task_call_func=self.__name__,
@ -133,7 +162,9 @@ class MonitoredTask(Task):
TaskInfo(
task_name=self.__name__,
task_description=self.__doc__,
finish_timestamp=datetime.now(),
start_timestamp=self.start,
finish_timestamp=default_timer(),
finish_time=datetime.now(),
result=self._result,
task_call_module=self.__module__,
task_call_func=self.__name__,
@ -151,3 +182,7 @@ class MonitoredTask(Task):
def run(self, *args, **kwargs):
raise NotImplementedError
for task in TaskInfo.all().values():
task.set_prom_metrics()

View File

@ -0,0 +1,26 @@
"""Test GeoIP Wrapper"""
from django.test import TestCase
from authentik.events.geo import GeoIPReader
class TestGeoIP(TestCase):
"""Test GeoIP Wrapper"""
def setUp(self) -> None:
self.reader = GeoIPReader()
def test_simple(self):
"""Test simple city wrapper"""
# IPs from
# https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
self.assertEqual(
self.reader.city_dict("2.125.160.216"),
{
"city": "Boxford",
"continent": "EU",
"country": "GB",
"lat": 51.75,
"long": -1.25,
},
)

View File

@ -2,6 +2,7 @@
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.flows.models import FlowStageBinding
@ -27,7 +28,7 @@ class FlowStageBindingSerializer(ModelSerializer):
]
class FlowStageBindingViewSet(ModelViewSet):
class FlowStageBindingViewSet(UsedByMixin, ModelViewSet):
"""FlowStageBinding Viewset"""
queryset = FlowStageBinding.objects.all()

View File

@ -6,10 +6,11 @@ from django.db.models import Model
from django.http.response import HttpResponseBadRequest, JsonResponse
from django.urls import reverse
from django.utils.translation import gettext as _
from drf_yasg import openapi
from drf_yasg.utils import no_body, swagger_auto_schema
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import BooleanField, FileField, ReadOnlyField
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
@ -23,6 +24,7 @@ from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import CacheSerializer, LinkSerializer
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow
@ -41,10 +43,18 @@ class FlowSerializer(ModelSerializer):
cache_count = SerializerMethodField()
def get_cache_count(self, flow: Flow):
background = ReadOnlyField(source="background_url")
export_url = SerializerMethodField()
def get_cache_count(self, flow: Flow) -> int:
"""Get count of cached flows"""
return len(cache.keys(f"{cache_key(flow)}*"))
def get_export_url(self, flow: Flow) -> str:
"""Get export URL for flow"""
return reverse("authentik_api:flow-export", kwargs={"slug": flow.slug})
class Meta:
model = Flow
@ -60,7 +70,12 @@ class FlowSerializer(ModelSerializer):
"policies",
"cache_count",
"policy_engine_mode",
"compatibility_mode",
"export_url",
]
extra_kwargs = {
"background": {"read_only": True},
}
class FlowDiagramSerializer(Serializer):
@ -87,7 +102,7 @@ class DiagramElement:
return f"{self.identifier}=>{self.type}: {self.rest}"
class FlowViewSet(ModelViewSet):
class FlowViewSet(UsedByMixin, ModelViewSet):
"""Flow Viewset"""
queryset = Flow.objects.all()
@ -97,16 +112,19 @@ class FlowViewSet(ModelViewSet):
filterset_fields = ["flow_uuid", "name", "slug", "designation"]
@permission_required(None, ["authentik_flows.view_flow_cache"])
@swagger_auto_schema(responses={200: CacheSerializer(many=False)})
@extend_schema(responses={200: CacheSerializer(many=False)})
@action(detail=False, pagination_class=None, filter_backends=[])
def cache_info(self, request: Request) -> Response:
"""Info about cached flows"""
return Response(data={"count": len(cache.keys("flow_*"))})
@permission_required(None, ["authentik_flows.clear_flow_cache"])
@swagger_auto_schema(
request_body=no_body,
responses={204: "Successfully cleared cache", 400: "Bad request"},
@extend_schema(
request=OpenApiTypes.NONE,
responses={
204: OpenApiResponse(description="Successfully cleared cache"),
400: OpenApiResponse(description="Bad request"),
},
)
@action(detail=False, methods=["POST"])
def cache_clear(self, request: Request) -> Response:
@ -133,17 +151,16 @@ class FlowViewSet(ModelViewSet):
"authentik_stages_prompt.change_prompt",
],
)
@swagger_auto_schema(
request_body=no_body,
manual_parameters=[
openapi.Parameter(
name="file",
in_=openapi.IN_FORM,
type=openapi.TYPE_FILE,
required=True,
@extend_schema(
request={
"multipart/form-data": inline_serializer(
"SetIcon", fields={"file": FileField()}
)
],
responses={204: "Successfully imported flow", 400: "Bad request"},
},
responses={
204: OpenApiResponse(description="Successfully imported flow"),
400: OpenApiResponse(description="Bad request"),
},
)
@action(detail=False, methods=["POST"], parser_classes=(MultiPartParser,))
def import_flow(self, request: Request) -> Response:
@ -157,8 +174,8 @@ class FlowViewSet(ModelViewSet):
return HttpResponseBadRequest()
successful = importer.apply()
if not successful:
return Response(status=204)
return HttpResponseBadRequest()
return HttpResponseBadRequest()
return Response(status=204)
@permission_required(
"authentik_flows.export_flow",
@ -171,11 +188,9 @@ class FlowViewSet(ModelViewSet):
"authentik_stages_prompt.view_prompt",
],
)
@swagger_auto_schema(
@extend_schema(
responses={
"200": openapi.Response(
"File Attachment", schema=openapi.Schema(type=openapi.TYPE_FILE)
),
"200": OpenApiResponse(response=OpenApiTypes.BINARY),
},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@ -188,7 +203,7 @@ class FlowViewSet(ModelViewSet):
response["Content-Disposition"] = f'attachment; filename="{flow.slug}.akflow"'
return response
@swagger_auto_schema(responses={200: FlowDiagramSerializer()})
@extend_schema(responses={200: FlowDiagramSerializer()})
@action(detail=True, pagination_class=None, filter_backends=[], methods=["get"])
# pylint: disable=unused-argument
def diagram(self, request: Request, slug: str) -> Response:
@ -259,17 +274,20 @@ class FlowViewSet(ModelViewSet):
return Response({"diagram": diagram})
@permission_required("authentik_flows.change_flow")
@swagger_auto_schema(
request_body=no_body,
manual_parameters=[
openapi.Parameter(
name="file",
in_=openapi.IN_FORM,
type=openapi.TYPE_FILE,
required=True,
@extend_schema(
request={
"multipart/form-data": inline_serializer(
"SetIcon",
fields={
"file": FileField(required=False),
"clear": BooleanField(default=False),
},
)
],
responses={200: "Success", 400: "Bad request"},
},
responses={
200: OpenApiResponse(description="Success"),
400: OpenApiResponse(description="Bad request"),
},
)
@action(
detail=True,
@ -281,16 +299,49 @@ class FlowViewSet(ModelViewSet):
# pylint: disable=unused-argument
def set_background(self, request: Request, slug: str):
"""Set Flow background"""
app: Flow = self.get_object()
icon = request.FILES.get("file", None)
if not icon:
flow: Flow = self.get_object()
background = request.FILES.get("file", None)
clear = request.data.get("clear", False)
if clear:
# .delete() saves the model by default
flow.background.delete()
return Response({})
if background:
flow.background = background
flow.save()
return Response({})
return HttpResponseBadRequest()
@permission_required("authentik_core.change_application")
@extend_schema(
request=inline_serializer("SetIconURL", fields={"url": CharField()}),
responses={
200: OpenApiResponse(description="Success"),
400: OpenApiResponse(description="Bad request"),
},
)
@action(
detail=True,
pagination_class=None,
filter_backends=[],
methods=["POST"],
)
# pylint: disable=unused-argument
def set_background_url(self, request: Request, slug: str):
"""Set Flow background (as URL)"""
flow: Flow = self.get_object()
url = request.data.get("url", None)
if not url:
return HttpResponseBadRequest()
app.background = icon
app.save()
flow.background.name = url
flow.save()
return Response({})
@swagger_auto_schema(
responses={200: LinkSerializer(many=False), 400: "Flow not applicable"},
@extend_schema(
responses={
200: LinkSerializer(many=False),
400: OpenApiResponse(description="Flow not applicable"),
},
)
@action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=unused-argument

View File

@ -1,16 +1,17 @@
"""Flow Stage API Views"""
from typing import Iterable
from drf_yasg.utils import swagger_auto_schema
from django.urls.base import reverse
from drf_spectacular.utils import extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import BooleanField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.types import UserSettingSerializer
from authentik.flows.api.flows import FlowSerializer
@ -20,12 +21,6 @@ from authentik.lib.utils.reflection import all_subclasses
LOGGER = get_logger()
class StageUserSettingSerializer(UserSettingSerializer):
"""User settings but can include a configure flow"""
configure_flow = BooleanField(required=False)
class StageSerializer(ModelSerializer, MetaNameSerializer):
"""Stage Serializer"""
@ -55,6 +50,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
class StageViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
@ -65,10 +61,10 @@ class StageViewSet(
search_fields = ["name"]
filterset_fields = ["name"]
def get_queryset(self):
def get_queryset(self): # pragma: no cover
return Stage.objects.select_subclasses()
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable stage types"""
@ -86,7 +82,7 @@ class StageViewSet(
data = sorted(data, key=lambda x: x["name"])
return Response(TypeCreateSerializer(data, many=True).data)
@swagger_auto_schema(responses={200: StageUserSettingSerializer(many=True)})
@extend_schema(responses={200: UserSettingSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def user_settings(self, request: Request) -> Response:
"""Get all stages the user can configure"""
@ -98,8 +94,9 @@ class StageViewSet(
continue
user_settings.initial_data["object_uid"] = str(stage.pk)
if hasattr(stage, "configure_flow"):
user_settings.initial_data["configure_flow"] = bool(
stage.configure_flow
user_settings.initial_data["configure_url"] = reverse(
"authentik_flows:configure",
kwargs={"stage_uuid": stage.pk},
)
if not user_settings.is_valid():
LOGGER.warning(user_settings.errors)

View File

@ -2,6 +2,9 @@
from importlib import import_module
from django.apps import AppConfig
from django.db.utils import ProgrammingError
from authentik.lib.utils.reflection import all_subclasses
class AuthentikFlowsConfig(AppConfig):
@ -14,3 +17,10 @@ class AuthentikFlowsConfig(AppConfig):
def ready(self):
import_module("authentik.flows.signals")
try:
from authentik.flows.models import Stage
for stage in all_subclasses(Stage):
_ = stage().type
except ProgrammingError:
pass

View File

@ -28,6 +28,14 @@ class ErrorDetailSerializer(PassiveSerializer):
code = CharField()
class ContextualFlowInfo(PassiveSerializer):
"""Contextual flow information for a challenge"""
title = CharField(required=False, allow_blank=True)
background = CharField(required=False)
cancel_url = CharField()
class Challenge(PassiveSerializer):
"""Challenge that gets sent to the client based on which stage
is currently active"""
@ -35,9 +43,8 @@ class Challenge(PassiveSerializer):
type = ChoiceField(
choices=[(x.value, x.name) for x in ChallengeTypes],
)
component = CharField(required=False)
title = CharField(required=False)
background = CharField(required=False)
flow_info = ContextualFlowInfo(required=False)
component = CharField(default="")
response_errors = DictField(
child=ErrorDetailSerializer(many=True), allow_empty=True, required=False
@ -48,18 +55,20 @@ class RedirectChallenge(Challenge):
"""Challenge type to redirect the client"""
to = CharField()
component = CharField(default="xak-flow-redirect")
class ShellChallenge(Challenge):
"""Legacy challenge type to render HTML as-is"""
"""challenge type to render HTML as-is"""
body = CharField()
component = CharField(default="xak-flow-shell")
class WithUserInfoChallenge(Challenge):
"""Challenge base which shows some user info"""
pending_user = CharField()
pending_user = CharField(allow_blank=True)
pending_user_avatar = CharField()
@ -67,6 +76,7 @@ class AccessDeniedChallenge(Challenge):
"""Challenge when a flow's active stage calls `stage_invalid()`."""
error_message = CharField(required=False)
component = CharField(default="ak-stage-access-denied")
class PermissionSerializer(PassiveSerializer):
@ -80,6 +90,7 @@ class ChallengeResponse(PassiveSerializer):
"""Base class for all challenge responses"""
stage: Optional["StageView"]
component = CharField(default="xak-flow-response-default")
def __init__(self, instance=None, data=None, **kwargs):
self.stage = kwargs.pop("stage", None)

View File

@ -21,7 +21,7 @@ context["user_backend"] = "django.contrib.auth.backends.ModelBackend"
return True"""
def create_default_oob_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
def create_default_oobe_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.stages.prompt.models import FieldTypes
User = apps.get_model("authentik_core", "User")
@ -52,20 +52,20 @@ def create_default_oob_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor)
# Create a policy that sets the flow's user
prefill_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
name="default-oob-prefill-user",
name="default-oobe-prefill-user",
defaults={"expression": PREFILL_POLICY_EXPRESSION},
)
password_usable_policy, _ = ExpressionPolicy.objects.using(
db_alias
).update_or_create(
name="default-oob-password-usable",
name="default-oobe-password-usable",
defaults={"expression": PW_USABLE_POLICY_EXPRESSION},
)
prompt_header, _ = Prompt.objects.using(db_alias).update_or_create(
field_key="oob-header-text",
field_key="oobe-header-text",
defaults={
"label": "oob-header-text",
"label": "oobe-header-text",
"type": FieldTypes.STATIC,
"placeholder": "Welcome to authentik! Please set a password for the default admin user, akadmin.",
"order": 100,
@ -84,7 +84,7 @@ def create_default_oob_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor)
password_second = Prompt.objects.using(db_alias).get(field_key="password_repeat")
prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create(
name="default-oob-password",
name="default-oobe-password",
)
prompt_stage.fields.set(
[prompt_header, prompt_email, password_first, password_second]
@ -102,7 +102,7 @@ def create_default_oob_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor)
slug="initial-setup",
designation=FlowDesignation.STAGE_CONFIGURATION,
defaults={
"name": "default-oob-setup",
"name": "default-oobe-setup",
"title": "Welcome to authentik!",
},
)
@ -146,5 +146,5 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(create_default_oob_flow),
migrations.RunPython(create_default_oobe_flow),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.3 on 2021-06-05 17:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0018_oob_flows"),
]
operations = [
migrations.AlterField(
model_name="flow",
name="background",
field=models.FileField(
default=None,
help_text="Background shown during execution",
null=True,
upload_to="flow-backgrounds/",
),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.3 on 2021-06-05 17:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0019_alter_flow_background"),
]
operations = [
migrations.AddField(
model_name="flow",
name="compatibility_mode",
field=models.BooleanField(
default=True,
help_text="Enable compatibility mode, increases compatibility with password managers on mobile devices.",
),
),
]

View File

@ -72,7 +72,7 @@ class Stage(SerializerModel):
def __str__(self):
if hasattr(self, "__in_memory_type"):
return f"In-memory Stage {getattr(self, '__in_memory_type')}"
return self.name
return f"Stage {self.name}"
def in_memory_stage(view: Type["StageView"]) -> Stage:
@ -110,11 +110,31 @@ class Flow(SerializerModel, PolicyBindingModel):
background = models.FileField(
upload_to="flow-backgrounds/",
default="../static/dist/assets/images/flow_background.jpg",
blank=True,
default=None,
null=True,
help_text=_("Background shown during execution"),
)
compatibility_mode = models.BooleanField(
default=True,
help_text=_(
"Enable compatibility mode, increases compatibility with "
"password managers on mobile devices."
),
)
@property
def background_url(self) -> str:
"""Get the URL to the background image. If the name is /static or starts with http
it is returned as-is"""
if not self.background:
return "/static/dist/assets/images/flow_background.jpg"
if self.background.name.startswith("http") or self.background.name.startswith(
"/static"
):
return self.background.name
return self.background.url
stages = models.ManyToManyField(Stage, through="FlowStageBinding", blank=True)
@property
@ -142,11 +162,6 @@ class Flow(SerializerModel, PolicyBindingModel):
LOGGER.debug("with_policy: no flow found", filters=flow_filter)
return None
def related_flow(self, designation: str, request: HttpRequest) -> Optional["Flow"]:
"""Get a related flow with `designation`. Currently this only queries
Flows by `designation`, but will eventually use `self` for related lookups."""
return Flow.with_policy(request, designation=designation)
def __str__(self) -> str:
return f"Flow {self.name} ({self.slug})"
@ -197,7 +212,7 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
return FlowStageBindingSerializer
def __str__(self) -> str:
return f"{self.target} #{self.order}"
return f"Flow-stage binding #{self.order} to {self.target}"
class Meta:

View File

@ -4,6 +4,7 @@ from typing import Any, Optional
from django.core.cache import cache
from django.http import HttpRequest
from prometheus_client import Histogram
from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span
from structlog.stdlib import BoundLogger, get_logger
@ -14,6 +15,7 @@ from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableExce
from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import Flow, FlowStageBinding, Stage
from authentik.policies.engine import PolicyEngine
from authentik.root.monitoring import UpdatingGauge
LOGGER = get_logger()
PLAN_CONTEXT_PENDING_USER = "pending_user"
@ -21,6 +23,16 @@ PLAN_CONTEXT_SSO = "is_sso"
PLAN_CONTEXT_REDIRECT = "redirect"
PLAN_CONTEXT_APPLICATION = "application"
PLAN_CONTEXT_SOURCE = "source"
GAUGE_FLOWS_CACHED = UpdatingGauge(
"authentik_flows_cached",
"Cached flows",
update_func=lambda: len(cache.keys("flow_*") or []),
)
HIST_FLOWS_PLAN_TIME = Histogram(
"authentik_flows_plan_time",
"Duration to build a plan for a flow",
["flow_slug"],
)
def cache_key(flow: Flow, user: Optional[User] = None) -> str:
@ -146,6 +158,7 @@ class FlowPlanner:
)
plan = self._build_plan(user, request, default_context)
cache.set(cache_key(self.flow, user), plan)
GAUGE_FLOWS_CACHED.update()
if not plan.stages and not self.allow_empty_flows:
raise EmptyFlowException()
return plan
@ -158,7 +171,9 @@ class FlowPlanner:
) -> FlowPlan:
"""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") as span:
with Hub.current.start_span(
op="flow.planner.build_plan"
) as span, HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug).time():
span: Span
span.set_data("flow", self.flow)
span.set_data("user", user)
@ -202,6 +217,7 @@ class FlowPlanner:
marker = ReevaluateMarker(binding=binding, user=user)
if stage:
plan.append(stage, marker)
HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug)
self._logger.debug(
"f(plan): finished building",
)

View File

@ -3,6 +3,7 @@ from django.contrib.auth.models import AnonymousUser
from django.http import HttpRequest
from django.http.request import QueryDict
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 structlog.stdlib import get_logger
@ -11,6 +12,7 @@ from authentik.core.models import DEFAULT_AVATAR, User
from authentik.flows.challenge import (
Challenge,
ChallengeResponse,
ContextualFlowInfo,
HttpChallengeResponse,
WithUserInfoChallenge,
)
@ -48,14 +50,17 @@ class StageView(View):
self.executor = executor
super().__init__(**kwargs)
def get_pending_user(self) -> User:
def get_pending_user(self, for_display=False) -> User:
"""Either show the matched User object or show what the user entered,
based on what the earlier stage (mostly IdentificationStage) set.
_USER_IDENTIFIER overrides the first User, as PENDING_USER is used for
other things besides the form display.
If no user is pending, returns request.user"""
if PLAN_CONTEXT_PENDING_USER_IDENTIFIER in self.executor.plan.context:
if (
PLAN_CONTEXT_PENDING_USER_IDENTIFIER in self.executor.plan.context
and for_display
):
return User(
username=self.executor.plan.context.get(
PLAN_CONTEXT_PENDING_USER_IDENTIFIER
@ -93,15 +98,21 @@ class ChallengeStageView(StageView):
def _get_challenge(self, *args, **kwargs) -> Challenge:
challenge = self.get_challenge(*args, **kwargs)
if "title" not in challenge.initial_data:
challenge.initial_data["title"] = self.executor.flow.title
if "background" not in challenge.initial_data:
challenge.initial_data["background"] = self.executor.flow.background.url
if "flow_info" not in challenge.initial_data:
flow_info = ContextualFlowInfo(
data={
"title": self.executor.flow.title,
"background": self.executor.flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
}
)
flow_info.is_valid()
challenge.initial_data["flow_info"] = flow_info.data
if isinstance(challenge, WithUserInfoChallenge):
# If there's a pending user, update the `username` field
# this field is only used by password managers.
# If there's no user set, an error is raised later.
if user := self.get_pending_user():
if user := self.get_pending_user(for_display=True):
challenge.initial_data["pending_user"] = user.username
challenge.initial_data["pending_user_avatar"] = DEFAULT_AVATAR
if not isinstance(user, AnonymousUser):

View File

@ -93,7 +93,11 @@ class TestFlowExecutor(TestCase):
{
"component": "ak-stage-access-denied",
"error_message": FlowNonApplicableException.__doc__,
"title": "",
"flow_info": {
"background": flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
"title": "",
},
"type": ChallengeTypes.NATIVE.value,
},
)
@ -289,7 +293,11 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
def test_reevaluate_keep(self):
@ -366,7 +374,11 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
def test_reevaluate_remove_consecutive(self):
@ -414,10 +426,13 @@ class TestFlowExecutor(TestCase):
self.assertJSONEqual(
force_str(response.content),
{
"background": flow.background.url,
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-dummy",
"title": binding.stage.name,
"flow_info": {
"background": flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
"title": "",
},
},
)
@ -445,10 +460,13 @@ class TestFlowExecutor(TestCase):
self.assertJSONEqual(
force_str(response.content),
{
"background": flow.background.url,
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-dummy",
"title": binding4.stage.name,
"flow_info": {
"background": flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
"title": "",
},
},
)
@ -458,7 +476,11 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
def test_stageview_user_identifier(self):
@ -489,4 +511,4 @@ class TestFlowExecutor(TestCase):
executor.flow = flow
stage_view = StageView(executor)
self.assertEqual(ident, stage_view.get_pending_user().username)
self.assertEqual(ident, stage_view.get_pending_user(for_display=True).username)

View File

@ -1,6 +1,5 @@
"""flow urls"""
from django.urls import path
from django.views.generic import RedirectView
from authentik.flows.models import FlowDesignation
from authentik.flows.views import CancelView, ConfigureFlowInitView, ToDefaultFlow
@ -16,30 +15,10 @@ urlpatterns = [
ToDefaultFlow.as_view(designation=FlowDesignation.INVALIDATION),
name="default-invalidation",
),
path(
"-/default/recovery/",
ToDefaultFlow.as_view(designation=FlowDesignation.RECOVERY),
name="default-recovery",
),
path(
"-/default/enrollment/",
ToDefaultFlow.as_view(designation=FlowDesignation.ENROLLMENT),
name="default-enrollment",
),
path(
"-/default/unenrollment/",
ToDefaultFlow.as_view(designation=FlowDesignation.UNRENOLLMENT),
name="default-unenrollment",
),
path("-/cancel/", CancelView.as_view(), name="cancel"),
path(
"-/configure/<uuid:stage_uuid>/",
ConfigureFlowInitView.as_view(),
name="configure",
),
path(
"<slug:flow_slug>/",
RedirectView.as_view(pattern_name="authentik_core:if-flow"),
name="flow-executor-shell",
),
]

View File

@ -2,16 +2,23 @@
from traceback import format_tb
from typing import Any, Optional
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect
from django.http.request import QueryDict
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls.base import reverse
from django.utils.decorators import method_decorator
from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.generic import View
from drf_yasg import openapi
from drf_yasg.utils import no_body, swagger_auto_schema
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter,
OpenApiResponse,
PolymorphicProxySerializer,
extend_schema,
)
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from sentry_sdk import capture_exception
@ -27,6 +34,7 @@ from authentik.flows.challenge import (
HttpChallengeResponse,
RedirectChallenge,
ShellChallenge,
WithUserInfoChallenge,
)
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
@ -36,8 +44,9 @@ from authentik.flows.planner import (
FlowPlan,
FlowPlanner,
)
from authentik.lib.utils.reflection import class_to_path
from authentik.lib.utils.reflection import all_subclasses, class_to_path
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
from authentik.tenants.models import Tenant
LOGGER = get_logger()
# Argument used to redirect user after login
@ -47,6 +56,43 @@ SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre"
SESSION_KEY_GET = "authentik_flows_get"
def challenge_types():
"""This is a workaround for PolymorphicProxySerializer not accepting a callable for
`serializers`. This function returns a class which is an iterator, which returns the
subclasses of Challenge, and Challenge itself."""
class Inner(dict):
"""dummy class with custom callback on .items()"""
def items(self):
mapping = {}
classes = all_subclasses(Challenge)
classes.remove(WithUserInfoChallenge)
for cls in classes:
mapping[cls().fields["component"].default] = cls
return mapping.items()
return Inner()
def challenge_response_types():
"""This is a workaround for PolymorphicProxySerializer not accepting a callable for
`serializers`. This function returns a class which is an iterator, which returns the
subclasses of Challenge, and Challenge itself."""
class Inner(dict):
"""dummy class with custom callback on .items()"""
def items(self):
mapping = {}
classes = all_subclasses(ChallengeResponse)
for cls in classes:
mapping[cls(stage=None).fields["component"].default] = cls
return mapping.items()
return Inner()
@method_decorator(xframe_options_sameorigin, name="dispatch")
class FlowExecutorView(APIView):
"""Stage 1 Flow executor, passing requests to Stage Views"""
@ -125,19 +171,25 @@ class FlowExecutorView(APIView):
self.current_stage_view.request = request
return super().dispatch(request)
@swagger_auto_schema(
@extend_schema(
responses={
200: Challenge(),
404: "No Token found", # This error can be raised by the email stage
200: PolymorphicProxySerializer(
component_name="FlowChallengeRequest",
serializers=challenge_types(),
resource_type_field_name="component",
),
404: OpenApiResponse(
description="No Token found"
), # This error can be raised by the email stage
},
request_body=no_body,
manual_parameters=[
openapi.Parameter(
"query",
openapi.IN_QUERY,
request=OpenApiTypes.NONE,
parameters=[
OpenApiParameter(
name="query",
location=OpenApiParameter.QUERY,
required=True,
description="Querystring as received",
type=openapi.TYPE_STRING,
type=OpenApiTypes.STR,
)
],
operation_id="flows_executor_get",
@ -153,20 +205,32 @@ class FlowExecutorView(APIView):
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
if settings.DEBUG or settings.TEST:
raise exc
capture_exception(exc)
self._logger.warning(exc)
return to_stage_response(request, FlowErrorResponse(request, exc))
@swagger_auto_schema(
responses={200: Challenge()},
request_body=ChallengeResponse(),
manual_parameters=[
openapi.Parameter(
"query",
openapi.IN_QUERY,
@extend_schema(
responses={
200: PolymorphicProxySerializer(
component_name="FlowChallengeRequest",
serializers=challenge_types(),
resource_type_field_name="component",
),
},
request=PolymorphicProxySerializer(
component_name="FlowChallengeResponse",
serializers=challenge_response_types(),
resource_type_field_name="component",
),
parameters=[
OpenApiParameter(
name="query",
location=OpenApiParameter.QUERY,
required=True,
description="Querystring as received",
type=openapi.TYPE_STRING,
type=OpenApiTypes.STR,
)
],
operation_id="flows_executor_solve",
@ -182,6 +246,8 @@ class FlowExecutorView(APIView):
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
if settings.DEBUG or settings.TEST:
raise exc
capture_exception(exc)
self._logger.warning(exc)
return to_stage_response(request, FlowErrorResponse(request, exc))
@ -218,7 +284,7 @@ class FlowExecutorView(APIView):
if self.plan.stages:
self._logger.debug(
"f(exec): Continuing with next stage",
reamining=len(self.plan.stages),
remaining=len(self.plan.stages),
)
kwargs = self.kwargs
kwargs.update({"flow_slug": self.flow.slug})
@ -244,9 +310,13 @@ class FlowExecutorView(APIView):
AccessDeniedChallenge(
{
"error_message": error_message,
"title": self.flow.title,
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-access-denied",
"flow_info": {
"title": self.flow.title,
"background": self.flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
},
}
)
)
@ -298,7 +368,7 @@ class CancelView(View):
if SESSION_KEY_PLAN in request.session:
del request.session[SESSION_KEY_PLAN]
LOGGER.debug("Canceled current plan")
return redirect("authentik_core:default-invalidation")
return redirect("authentik_flows:default-invalidation")
class ToDefaultFlow(View):
@ -307,7 +377,17 @@ class ToDefaultFlow(View):
designation: Optional[FlowDesignation] = None
def dispatch(self, request: HttpRequest) -> HttpResponse:
flow = Flow.with_policy(request, designation=self.designation)
tenant: Tenant = request.tenant
flow = None
# First, attempt to get default flow from tenant
if self.designation == FlowDesignation.AUTHENTICATION:
flow = tenant.flow_authentication
if self.designation == FlowDesignation.INVALIDATION:
flow = tenant.flow_invalidation
# If no flow was set, get the first based on slug and policy
if not flow:
flow = Flow.with_policy(request, designation=self.designation)
# If we still don't have a flow, 404
if not flow:
raise Http404
# If user already has a pending plan, clear it so we don't have to later.
@ -338,7 +418,10 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
)
return HttpChallengeResponse(
RedirectChallenge(
{"type": ChallengeTypes.REDIRECT, "to": str(redirect_url)}
{
"type": ChallengeTypes.REDIRECT,
"to": str(redirect_url),
}
)
)
if isinstance(source, TemplateResponse):
@ -350,7 +433,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
}
)
)
# Check for actual HttpResponse (without isinstance as we dont want to check inheritance)
# Check for actual HttpResponse (without isinstance as we don't want to check inheritance)
if source.__class__ == HttpResponse:
return HttpChallengeResponse(
ShellChallenge(

View File

@ -10,9 +10,6 @@ from urllib.parse import urlparse
import yaml
from django.conf import ImproperlyConfigured
from django.http import HttpRequest
from authentik import __version__
SEARCH_PATHS = ["authentik/lib/default.yml", "/etc/authentik/config.yml", ""] + glob(
"/etc/authentik/config.d/*.yml", recursive=True
@ -21,11 +18,6 @@ ENV_PREFIX = "AUTHENTIK"
ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local")
def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects config object into every template"""
return {"config": CONFIG.raw, "ak_version": __version__}
class ConfigLoader:
"""Search through SEARCH_PATHS and load configuration. Environment variables starting with
`ENV_PREFIX` are also applied.
@ -88,10 +80,10 @@ class ConfigLoader:
value = os.getenv(url.netloc, url.query)
if url.scheme == "file":
try:
with open(url.netloc, "r") as _file:
with open(url.path, "r") as _file:
value = _file.read()
except OSError:
self._log("error", f"Failed to read config value from {url.netloc}")
self._log("error", f"Failed to read config value from {url.path}")
value = url.query
return value

View File

@ -47,10 +47,7 @@ outposts:
authentik:
avatars: gravatar # gravatar or none
geoip: ""
branding:
title: authentik
logo: /static/dist/assets/icons/icon_left_brand.svg
geoip: "./GeoLite2-City.mmdb"
# Optionally add links to the footer on the login page
footer_links:
- name: Documentation

View File

@ -26,8 +26,8 @@ class BaseEvaluator:
_filename: str
def __init__(self):
# update authentik/policies/expression/templates/policy/expression/form.html
# update website/docs/policies/expression.md
# update website/docs/expressions/_objects.md
# update website/docs/expressions/_functions.md
self._globals = {
"regex_match": BaseEvaluator.expr_filter_regex_match,
"regex_replace": BaseEvaluator.expr_filter_regex_replace,

View File

@ -8,7 +8,11 @@ from botocore.exceptions import BotoCoreError
from celery.exceptions import CeleryError
from channels.middleware import BaseMiddleware
from channels_redis.core import ChannelFull
from django.core.exceptions import SuspiciousOperation, ValidationError
from django.core.exceptions import (
ImproperlyConfigured,
SuspiciousOperation,
ValidationError,
)
from django.db import InternalError, OperationalError, ProgrammingError
from django.http.response import Http404
from django_redis.exceptions import ConnectionInterrupted
@ -51,7 +55,8 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
ConnectionResetError,
OSError,
PermissionError,
# Django DB Errors
# Django Errors
ImproperlyConfigured,
OperationalError,
InternalError,
ProgrammingError,
@ -89,6 +94,13 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
if isinstance(exc_value, ignored_classes):
return None
if "logger" in event:
if event["logger"] in ["dbbackup", "botocore"]:
if event["logger"] in [
"dbbackup",
"botocore",
"kombu",
"asyncio",
"multiprocessing",
"django_redis",
]:
return None
return event

View File

@ -5,9 +5,10 @@ from django.http import HttpRequest
OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP"
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
DEFAULT_IP = "255.255.255.255"
def _get_client_ip_from_meta(meta: dict[str, Any]) -> Optional[str]:
def _get_client_ip_from_meta(meta: dict[str, Any]) -> str:
"""Attempt to get the client's IP by checking common HTTP Headers.
Returns none if no IP Could be found"""
headers = (
@ -17,8 +18,9 @@ def _get_client_ip_from_meta(meta: dict[str, Any]) -> Optional[str]:
)
for _header in headers:
if _header in meta:
return meta.get(_header).split(", ")[0]
return None
ips: list[str] = meta.get(_header).split(",")
return ips[0].strip()
return DEFAULT_IP
def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
@ -36,7 +38,7 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
return request.META[OUTPOST_REMOTE_IP_HEADER]
def get_client_ip(request: Optional[HttpRequest]) -> Optional[str]:
def get_client_ip(request: Optional[HttpRequest]) -> str:
"""Attempt to get the client's IP by checking common HTTP Headers.
Returns none if no IP Could be found"""
if request:
@ -44,4 +46,4 @@ def get_client_ip(request: Optional[HttpRequest]) -> Optional[str]:
if override:
return override
return _get_client_ip_from_meta(request.META)
return None
return DEFAULT_IP

View File

@ -2,28 +2,6 @@
from django.http import HttpRequest
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView
from guardian.shortcuts import assign_perm
class CreateAssignPermView(CreateView):
"""Assign permissions to object after creation"""
permissions = [
"%s.view_%s",
"%s.change_%s",
"%s.delete_%s",
]
def form_valid(self, form):
response = super().form_valid(form)
for permission in self.permissions:
full_permission = permission % (
self.object._meta.app_label,
self.object._meta.model_name,
)
assign_perm(full_permission, self.request.user, self.object)
return response
def bad_request_message(

View File

@ -6,7 +6,7 @@ class AuthentikManagedConfig(AppConfig):
"""authentik Managed app"""
name = "authentik.managed"
label = "authentik_Managed"
label = "authentik_managed"
verbose_name = "authentik Managed"
def ready(self) -> None:

View File

@ -1,24 +1,60 @@
"""Outpost API Views"""
from dacite.core import from_dict
from dacite.exceptions import DaciteError
from drf_yasg.utils import swagger_auto_schema
from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, DateTimeField
from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import JSONField, ModelSerializer, 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, is_dict
from authentik.outposts.models import Outpost, OutpostConfig, default_outpost_config
from authentik.core.models import Provider
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
from authentik.outposts.models import (
Outpost,
OutpostConfig,
OutpostType,
default_outpost_config,
)
from authentik.providers.ldap.models import LDAPProvider
from authentik.providers.proxy.models import ProxyProvider
class OutpostSerializer(ModelSerializer):
"""Outpost Serializer"""
config = JSONField(validators=[is_dict], source="_config")
providers = PrimaryKeyRelatedField(
allow_empty=False,
many=True,
queryset=Provider.objects.select_subclasses().all(),
)
providers_obj = ProviderSerializer(source="providers", many=True, read_only=True)
service_connection_obj = ServiceConnectionSerializer(
source="service_connection", read_only=True
)
def validate_providers(self, providers: list[Provider]) -> list[Provider]:
"""Check that all providers match the type of the outpost"""
type_map = {
OutpostType.LDAP: LDAPProvider,
OutpostType.PROXY: ProxyProvider,
None: Provider,
}
for provider in providers:
if not isinstance(provider, type_map[self.initial_data.get("type")]):
raise ValidationError(
(
f"Outpost type {self.initial_data['type']} can't be used with "
f"{type(provider)} providers."
)
)
return providers
def validate_config(self, config) -> dict:
"""Check that the config has all required fields"""
@ -38,9 +74,11 @@ class OutpostSerializer(ModelSerializer):
"providers",
"providers_obj",
"service_connection",
"service_connection_obj",
"token_identifier",
"config",
]
extra_kwargs = {"type": {"required": True}}
class OutpostDefaultConfigSerializer(PassiveSerializer):
@ -58,7 +96,7 @@ class OutpostHealthSerializer(PassiveSerializer):
version_outdated = BooleanField(read_only=True)
class OutpostViewSet(ModelViewSet):
class OutpostViewSet(UsedByMixin, ModelViewSet):
"""Outpost Viewset"""
queryset = Outpost.objects.all()
@ -70,10 +108,10 @@ class OutpostViewSet(ModelViewSet):
"name",
"providers__name",
]
ordering = ["name"]
ordering = ["name", "service_connection__name"]
@swagger_auto_schema(responses={200: OutpostHealthSerializer(many=True)})
@action(methods=["GET"], detail=True)
@extend_schema(responses={200: OutpostHealthSerializer(many=True)})
@action(methods=["GET"], detail=True, pagination_class=None)
# pylint: disable=invalid-name, unused-argument
def health(self, request: Request, pk: int) -> Response:
"""Get outposts current health"""
@ -90,7 +128,7 @@ class OutpostViewSet(ModelViewSet):
)
return Response(OutpostHealthSerializer(states, many=True).data)
@swagger_auto_schema(responses={200: OutpostDefaultConfigSerializer(many=False)})
@extend_schema(responses={200: OutpostDefaultConfigSerializer(many=False)})
@action(detail=False, methods=["GET"])
def default_settings(self, request: Request) -> Response:
"""Global default outpost config"""

View File

@ -2,18 +2,19 @@
from dataclasses import asdict
from django.utils.translation import gettext_lazy as _
from drf_yasg.utils import swagger_auto_schema
from drf_spectacular.utils import extend_schema
from kubernetes.client.configuration import Configuration
from kubernetes.config.config_exception import ConfigException
from kubernetes.config.kube_config import load_kube_config_from_dict
from rest_framework import mixins, serializers
from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, SerializerMethodField
from rest_framework.fields import BooleanField, CharField, ReadOnlyField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
MetaNameSerializer,
PassiveSerializer,
@ -30,11 +31,7 @@ from authentik.outposts.models import (
class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
"""ServiceConnection Serializer"""
component = SerializerMethodField()
def get_component(self, obj: OutpostServiceConnection) -> str:
"""Get object component so that we know how to edit the object"""
return obj.component
component = ReadOnlyField()
class Meta:
@ -59,6 +56,7 @@ class ServiceConnectionStateSerializer(PassiveSerializer):
class ServiceConnectionViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
@ -69,7 +67,7 @@ class ServiceConnectionViewSet(
search_fields = ["name"]
filterset_fields = ["name"]
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def types(self, request: Request) -> Response:
"""Get all creatable service connection types"""
@ -87,7 +85,7 @@ class ServiceConnectionViewSet(
)
return Response(TypeCreateSerializer(data, many=True).data)
@swagger_auto_schema(responses={200: ServiceConnectionStateSerializer(many=False)})
@extend_schema(responses={200: ServiceConnectionStateSerializer(many=False)})
@action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=unused-argument, invalid-name
def state(self, request: Request, pk: str) -> Response:
@ -109,7 +107,7 @@ class DockerServiceConnectionSerializer(ServiceConnectionSerializer):
]
class DockerServiceConnectionViewSet(ModelViewSet):
class DockerServiceConnectionViewSet(UsedByMixin, ModelViewSet):
"""DockerServiceConnection Viewset"""
queryset = DockerServiceConnection.objects.all()
@ -122,7 +120,7 @@ class KubernetesServiceConnectionSerializer(ServiceConnectionSerializer):
def validate_kubeconfig(self, kubeconfig):
"""Validate kubeconfig by attempting to load it"""
if kubeconfig == {}:
if not self.validated_data["local"]:
if not self.initial_data["local"]:
raise serializers.ValidationError(
_(
"You can only use an empty kubeconfig when connecting to a local cluster."
@ -143,7 +141,7 @@ class KubernetesServiceConnectionSerializer(ServiceConnectionSerializer):
fields = ServiceConnectionSerializer.Meta.fields + ["kubeconfig"]
class KubernetesServiceConnectionViewSet(ModelViewSet):
class KubernetesServiceConnectionViewSet(UsedByMixin, ModelViewSet):
"""KubernetesServiceConnection Viewset"""
queryset = KubernetesServiceConnection.objects.all()

View File

@ -8,11 +8,21 @@ from channels.exceptions import DenyConnection
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 authentik.core.channels import AuthJsonConsumer
from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState
GAUGE_OUTPOSTS_CONNECTED = Gauge(
"authentik_outposts_connected", "Currently connected outposts", ["outpost", "uid"]
)
GAUGE_OUTPOSTS_LAST_UPDATE = Gauge(
"authentik_outposts_last_update",
"Last update from any outpost",
["outpost", "uid", "version"],
)
LOGGER = get_logger()
@ -44,6 +54,8 @@ class OutpostConsumer(AuthJsonConsumer):
last_uid: Optional[str] = None
first_msg = False
def connect(self):
super().connect()
uuid = self.scope["url_route"]["kwargs"]["pk"]
@ -64,7 +76,14 @@ class OutpostConsumer(AuthJsonConsumer):
# pylint: disable=unused-argument
def disconnect(self, close_code):
if self.outpost and self.last_uid:
OutpostState.for_channel(self.outpost, self.last_uid).delete()
state = OutpostState.for_instance_uid(self.outpost, self.last_uid)
if self.channel_name in state.channel_ids:
state.channel_ids.remove(self.channel_name)
state.save()
GAUGE_OUTPOSTS_CONNECTED.labels(
outpost=self.outpost.name,
uid=self.last_uid,
).dec()
LOGGER.debug(
"removed outpost instance from cache",
outpost=self.outpost,
@ -75,16 +94,32 @@ class OutpostConsumer(AuthJsonConsumer):
msg = from_dict(WebsocketMessage, content)
uid = msg.args.get("uuid", self.channel_name)
self.last_uid = uid
state = OutpostState(
uid=uid,
last_seen=datetime.now(),
_outpost=self.outpost,
)
if not self.outpost:
raise DenyConnection()
state = OutpostState.for_instance_uid(self.outpost, uid)
if self.channel_name not in state.channel_ids:
state.channel_ids.append(self.channel_name)
state.last_seen = datetime.now()
if not self.first_msg:
GAUGE_OUTPOSTS_CONNECTED.labels(
outpost=self.outpost.name,
uid=self.last_uid,
).inc()
self.first_msg = True
if msg.instruction == WebsocketMessageInstruction.HELLO:
state.version = msg.args.get("version", None)
state.build_hash = msg.args.get("buildHash", "")
elif msg.instruction == WebsocketMessageInstruction.ACK:
return
GAUGE_OUTPOSTS_LAST_UPDATE.labels(
outpost=self.outpost.name,
uid=self.last_uid or "",
version=state.version or "",
).set_to_current_time()
state.save(timeout=OUTPOST_HELLO_INTERVAL * 1.5)
response = WebsocketMessage(instruction=WebsocketMessageInstruction.ACK)

View File

@ -71,6 +71,7 @@ class DockerController(BaseController):
},
"environment": self._get_env(),
"labels": self._get_labels(),
"restart_policy": {"Name": "unless-stopped"},
}
if settings.TEST:
del container_args["ports"]
@ -80,27 +81,39 @@ class DockerController(BaseController):
True,
)
# pylint: disable=too-many-return-statements
def up(self):
try:
container, has_been_created = self._get_container()
if has_been_created:
return None
# Check if the container is out of date, delete it and retry
if len(container.image.tags) > 0:
tag: str = container.image.tags[0]
_, _, version = tag.partition(":")
if version != __version__:
if tag != self.get_container_image():
self.logger.info(
"Container has mismatched version, re-creating...",
has=version,
should=__version__,
"Container has mismatched image, re-creating...",
has=tag,
should=self.get_container_image(),
)
container.kill()
container.remove(force=True)
self.down()
return self.up()
# Check that container values match our values
if self._comp_env(container):
self.logger.info("Container has outdated config, re-creating...")
container.kill()
container.remove(force=True)
self.down()
return self.up()
if (
container.attrs.get("HostConfig", {})
.get("RestartPolicy", {})
.get("Name", "")
.lower()
!= "unless-stopped"
):
self.logger.info(
"Container has mis-matched restart policy, re-creating..."
)
self.down()
return self.up()
# Check that container is healthy
if (
@ -127,16 +140,16 @@ class DockerController(BaseController):
return None
return None
except DockerException as exc:
raise ControllerException from exc
raise ControllerException(str(exc)) from exc
def down(self):
try:
container, _ = self._get_container()
if container.status == "running":
container.kill()
container.remove()
container.remove(force=True)
except DockerException as exc:
raise ControllerException from exc
raise ControllerException(str(exc)) from exc
def get_static_deployment(self) -> str:
"""Generate docker-compose yaml for proxy, version 3.5"""

View File

@ -376,16 +376,24 @@ class Outpost(models.Model):
@property
def token(self) -> Token:
"""Get/create token for auto-generated user"""
token = Token.filter_not_expired(user=self.user, intent=TokenIntents.INTENT_API)
if token.exists():
return token.first()
managed = f"goauthentik.io/outpost/{self.token_identifier}"
tokens = Token.filter_not_expired(
identifier=self.token_identifier,
intent=TokenIntents.INTENT_API,
)
if tokens.exists():
token = tokens.first()
if not token.managed:
token.managed = managed
token.save()
return token
return Token.objects.create(
user=self.user,
identifier=self.token_identifier,
intent=TokenIntents.INTENT_API,
description=f"Autogenerated by authentik for Outpost {self.name}",
expiring=False,
managed=f"goauthentik.io/outpost/{self.token_identifier}",
managed=managed,
)
def get_required_objects(self) -> Iterable[Union[models.Model, str]]:
@ -409,6 +417,7 @@ class OutpostState:
"""Outpost instance state, last_seen and version"""
uid: str
channel_ids: list[str] = field(default_factory=list)
last_seen: Optional[datetime] = field(default=None)
version: Optional[str] = field(default=None)
version_should: Union[Version, LegacyVersion] = field(default=OUR_VERSION)
@ -431,21 +440,20 @@ class OutpostState:
keys = cache.keys(f"{outpost.state_cache_prefix}_*")
states = []
for key in keys:
channel = key.replace(f"{outpost.state_cache_prefix}_", "")
states.append(OutpostState.for_channel(outpost, channel))
instance_uid = key.replace(f"{outpost.state_cache_prefix}_", "")
states.append(OutpostState.for_instance_uid(outpost, instance_uid))
return states
@staticmethod
def for_channel(outpost: Outpost, channel: str) -> "OutpostState":
"""Get state for a single channel"""
key = f"{outpost.state_cache_prefix}_{channel}"
default_data = {"uid": channel}
def for_instance_uid(outpost: Outpost, uid: str) -> "OutpostState":
"""Get state for a single instance"""
key = f"{outpost.state_cache_prefix}_{uid}"
default_data = {"uid": uid, "channel_ids": []}
data = cache.get(key, default_data)
if isinstance(data, str):
cache.delete(key)
data = default_data
state = from_dict(OutpostState, data)
state.uid = channel
# pylint: disable=protected-access
state._outpost = outpost
return state

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