Compare commits

...

120 Commits

Author SHA1 Message Date
e23afd18e4 release: 0.13.3-stable 2020-12-19 16:55:07 +01:00
c2a30b760a web: allow Sidebar to be opened on mobile (#417)
* web: initial sidebar trigger on mobile

* web: render hamburger button as overlay top right
2020-12-19 16:54:25 +01:00
6e24856d45 flows: fix redirect when un-authenticated user uses external authentication (#416)
* flows: add PLAN_CONTEXT_REDIRECT so final redirect can be set from within flow

* sources/*: use PLAN_CONTEXT_REDIRECT

* flows: fallback when flow plan is empty
2020-12-19 16:42:39 +01:00
98a58b74e3 core: ensure generic error template fills screen 2020-12-19 14:28:20 +01:00
5f3ab22bea providers/oauth2: fix incorrect background set on end session screen 2020-12-19 14:24:28 +01:00
1ed5d5da35 build(deps): bump @sentry/browser from 5.29.1 to 5.29.2 in /web (#413)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 5.29.1 to 5.29.2.
- [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/5.29.1...5.29.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-18 09:58:34 +01:00
76193e0031 build(deps): bump boto3 from 1.16.38 to 1.16.39 (#412)
Bumps [boto3](https://github.com/boto/boto3) from 1.16.38 to 1.16.39.
- [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.16.38...1.16.39)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-18 09:44:54 +01:00
50109ca7ad build(deps): bump @sentry/tracing from 5.29.1 to 5.29.2 in /web (#414)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 5.29.1 to 5.29.2.
- [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/5.29.1...5.29.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-18 09:44:33 +01:00
e4b66d991c release: 0.13.2-stable 2020-12-17 20:20:47 +01:00
68adc2d5a5 admin: fix warning during swagger generation 2020-12-17 19:49:35 +01:00
349a3a67d5 flows: use to_stage_response in _flow_done() 2020-12-17 19:34:15 +01:00
e1394207e7 flows: fix inconsistent behaviour when flow is empty 2020-12-17 19:22:24 +01:00
f265c1f10b admin: fix cache clean views erroring 2020-12-17 19:03:32 +01:00
1aecdc7f8f web: fix css for policy tertiary buttons and text on flow card 2020-12-17 14:31:45 +01:00
a18edaf62b build(deps): bump @sentry/tracing from 5.29.0 to 5.29.1 in /web (#411)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 5.29.0 to 5.29.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/5.29.0...5.29.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-17 11:41:19 +01:00
c91abe448c build(deps): bump celery from 5.0.4 to 5.0.5 (#407)
Bumps [celery](https://github.com/celery/celery) from 5.0.4 to 5.0.5.
- [Release notes](https://github.com/celery/celery/releases)
- [Changelog](https://github.com/celery/celery/blob/master/Changelog.rst)
- [Commits](https://github.com/celery/celery/compare/v5.0.4...v5.0.5)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-17 09:46:11 +01:00
e531e52403 build(deps): bump django-storages from 1.10.1 to 1.11 (#408)
Bumps [django-storages](https://github.com/jschneier/django-storages) from 1.10.1 to 1.11.
- [Release notes](https://github.com/jschneier/django-storages/releases)
- [Changelog](https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jschneier/django-storages/compare/1.10.1...1.11)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-17 09:45:58 +01:00
cae536fa65 build(deps): bump boto3 from 1.16.37 to 1.16.38 (#409)
Bumps [boto3](https://github.com/boto/boto3) from 1.16.37 to 1.16.38.
- [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.16.37...1.16.38)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-17 09:45:45 +01:00
316b15b8a9 build(deps): bump @sentry/browser from 5.29.0 to 5.29.1 in /web (#410)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 5.29.0 to 5.29.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/5.29.0...5.29.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-17 09:45:29 +01:00
e6ccd4fa76 web: fix file name casing 2020-12-17 00:18:24 +01:00
86aabba3ed web: fix file name casing 2020-12-17 00:18:03 +01:00
0b36aad5c8 admin: ensure clean_expired_models is called during tests 2020-12-17 00:17:20 +01:00
64d2a216f0 web: fix linting 2020-12-16 23:50:23 +01:00
a5e5e140d6 admin: add full api tests 2020-12-16 23:42:44 +01:00
29f98abd00 root: update swagger 2020-12-16 23:32:14 +01:00
7b5ce4e98a web: use colours for icons, move users to separate card 2020-12-16 23:28:04 +01:00
d7fa52ebf3 admin: remove old admin overview 2020-12-16 23:21:38 +01:00
2ffaa94825 web: fix typo 2020-12-16 23:08:40 +01:00
b80b2626a6 web: fix rendering of version 2020-12-16 23:08:35 +01:00
3b7bba5a62 web: make sure naming matches backend 2020-12-16 23:03:06 +01:00
2d9efe035e web: migrate admin overview cards to separate files 2020-12-16 23:00:32 +01:00
48438e28fd admin: separate overview API into WorkerAPI and VersionAPI 2020-12-16 22:53:53 +01:00
885a2f0a58 web: add flow and policy cache card 2020-12-16 22:30:37 +01:00
cf46ee06b7 api: create dedicated api for cached flows and policies 2020-12-16 22:18:36 +01:00
9e33b49d29 web: rewrite aggregate cards to separate components 2020-12-16 22:00:40 +01:00
1179ba4ef2 api: remove counters from overview api and allow filtering on object apis 2020-12-16 22:00:29 +01:00
3c12c8b3ff core: make Provider SerializerModel 2020-12-16 21:38:40 +01:00
4d22659b6e web: re-organise sidebar 2020-12-16 16:04:11 +01:00
2c0709eeee web: render SidebarItem from the item 2020-12-16 16:04:02 +01:00
c24d1b6b84 outposts: fix incorrect timeout for state cache 2020-12-16 12:14:34 +01:00
040e148a73 release: 0.13.1-stable 2020-12-16 11:26:15 +01:00
b85d550ee0 build(deps-dev): bump pytest from 6.2.0 to 6.2.1 (#405)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.2.0...6.2.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-16 09:15:14 +01:00
ce95139d66 build(deps): bump boto3 from 1.16.36 to 1.16.37 (#404)
Bumps [boto3](https://github.com/boto/boto3) from 1.16.36 to 1.16.37.
- [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.16.36...1.16.37)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-16 09:14:58 +01:00
46436a5780 build(deps): bump @types/chart.js from 2.9.28 to 2.9.29 in /web (#406)
Bumps [@types/chart.js](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chart.js) from 2.9.28 to 2.9.29.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chart.js)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-16 09:14:43 +01:00
835a9aaaf2 outposts: fix circular import 2020-12-16 00:00:36 +01:00
42005e7def outposts: ensure all Service Connection state updates are done by the task 2020-12-15 23:39:52 +01:00
d9956e1e9c outpost: fix invalid incluster config causing Outpost Service Connection list to fail 2020-12-15 21:17:33 +01:00
4b1e73251a root: fix messages showing for all sessions of a user 2020-12-15 15:19:15 +01:00
736dbdca33 build(deps-dev): bump @rollup/plugin-typescript in /web (#401)
Bumps [@rollup/plugin-typescript](https://github.com/rollup/plugins) from 8.0.0 to 8.1.0.
- [Release notes](https://github.com/rollup/plugins/releases)
- [Commits](https://github.com/rollup/plugins/compare/eslint-v8.0.0...typescript-v8.1.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-15 10:54:31 +01:00
789b8e5d3e build(deps-dev): bump @typescript-eslint/eslint-plugin in /web (#402)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.9.1 to 4.10.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.10.0/packages/eslint-plugin)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-15 10:28:11 +01:00
074b55f66b build(deps): bump boto3 from 1.16.35 to 1.16.36 (#398)
Bumps [boto3](https://github.com/boto/boto3) from 1.16.35 to 1.16.36.
- [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.16.35...1.16.36)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-15 08:59:52 +01:00
d9bc5ea4d1 build(deps): bump rollup from 2.34.2 to 2.35.1 in /web (#399)
Bumps [rollup](https://github.com/rollup/rollup) from 2.34.2 to 2.35.1.
- [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.34.2...v2.35.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-15 08:59:42 +01:00
716bb9f188 build(deps): bump @patternfly/patternfly from 4.65.6 to 4.70.2 in /web (#400)
Bumps [@patternfly/patternfly](https://github.com/patternfly/patternfly) from 4.65.6 to 4.70.2.
- [Release notes](https://github.com/patternfly/patternfly/releases)
- [Changelog](https://github.com/patternfly/patternfly/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/patternfly/patternfly/compare/prerelease-v4.65.6...prerelease-v4.70.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-15 08:59:30 +01:00
dd496619a2 build(deps-dev): bump @typescript-eslint/parser in /web (#403)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.9.1 to 4.10.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.10.0/packages/parser)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-15 08:59:04 +01:00
51d07f7913 proxy: output JSON logs 2020-12-14 19:41:32 +01:00
5c4163579b root: fix application icons now showing with docker-compose 2020-12-14 19:32:48 +01:00
5a73413d58 web: fix brand not showing on firefox 2020-12-14 19:26:02 +01:00
51a5d4bf49 docs: fix issues when overscrolling 2020-12-14 14:16:00 +01:00
8bbb854073 root: make docker-compose database name and username configurable 2020-12-14 12:27:33 +01:00
9f2e9e8444 release: 0.13.0-stable 2020-12-14 11:20:47 +01:00
a3d361f500 outposts: fix controller not using token.key 2020-12-14 11:03:49 +01:00
e9bb583b32 providers/proxy: ensure pb_proxy is deleted and ak_proxy is created 2020-12-14 10:47:49 +01:00
efccf47c83 build(deps): bump packaging from 20.7 to 20.8 (#388)
Bumps [packaging](https://github.com/pypa/packaging) from 20.7 to 20.8.
- [Release notes](https://github.com/pypa/packaging/releases)
- [Changelog](https://github.com/pypa/packaging/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pypa/packaging/compare/20.7...20.8)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-14 10:07:23 +01:00
a5b144cf8f build(deps): bump boto3 from 1.16.34 to 1.16.35 (#391)
Bumps [boto3](https://github.com/boto/boto3) from 1.16.34 to 1.16.35.
- [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.16.34...1.16.35)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-14 10:07:12 +01:00
afc5a17fc2 build(deps): bump github.com/recws-org/recws in /proxy (#394)
Bumps [github.com/recws-org/recws](https://github.com/recws-org/recws) from 1.2.1 to 1.2.2.
- [Release notes](https://github.com/recws-org/recws/releases)
- [Commits](https://github.com/recws-org/recws/compare/v1.2.1...v1.2.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-14 10:06:59 +01:00
b3e0884b2e build(deps-dev): bump eslint-plugin-lit from 1.2.4 to 1.3.0 in /web (#396)
Bumps [eslint-plugin-lit](https://github.com/43081j/eslint-plugin-lit) from 1.2.4 to 1.3.0.
- [Release notes](https://github.com/43081j/eslint-plugin-lit/releases)
- [Commits](https://github.com/43081j/eslint-plugin-lit/compare/v1.2.4...v1.3.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-14 10:06:34 +01:00
078d648551 build(deps): bump uvicorn from 0.13.0 to 0.13.1 (#390)
* build(deps): bump uvicorn from 0.13.0 to 0.13.1

Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.13.0 to 0.13.1.
- [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.0...0.13.1)

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

* root: remove asgi workaround when websocket is closed during connect

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens.langhammer@beryju.org>
2020-12-14 10:05:07 +01:00
41f9097592 build(deps-dev): bump pytest from 6.1.2 to 6.2.0 (#389)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.1.2 to 6.2.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.1.2...6.2.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-14 08:53:18 +01:00
562175741c build(deps): bump github.com/getsentry/sentry-go in /proxy (#392)
Bumps [github.com/getsentry/sentry-go](https://github.com/getsentry/sentry-go) from 0.7.0 to 0.9.0.
- [Release notes](https://github.com/getsentry/sentry-go/releases)
- [Changelog](https://github.com/getsentry/sentry-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-go/compare/v0.7.0...v0.9.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-14 08:52:56 +01:00
24e24cb97e build(deps-dev): bump typescript from 4.1.2 to 4.1.3 in /web (#395)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/commits)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-14 08:52:40 +01:00
69b0a23a7d build(deps-dev): bump bandit from 1.6.3 to 1.7.0 (#387)
Bumps [bandit](https://github.com/PyCQA/bandit) from 1.6.3 to 1.7.0.
- [Release notes](https://github.com/PyCQA/bandit/releases)
- [Commits](https://github.com/PyCQA/bandit/compare/1.6.3...1.7.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-14 08:11:24 +01:00
f0f3245388 root: fix links to docs 2020-12-14 00:45:02 +01:00
99ca0d1f9f helm: fix missing /media/ route 2020-12-14 00:11:49 +01:00
c9f0d048a8 release: 0.13.0-rc4 2020-12-13 23:40:07 +01:00
90a94b5e3e root: fix paths for XML Schema files 2020-12-13 23:40:01 +01:00
ae1a8842db providers/oauth2: start adding tests for OAuthAuthorizationParams 2020-12-13 23:14:35 +01:00
a3b17d1ed4 admin: add tests for hidden form fields 2020-12-13 23:14:18 +01:00
41576e27be tests/integration: continue even if ssl can't be cleaned up 2020-12-13 21:51:59 +01:00
07082cb3aa tests/integration: add tests for Docker outpost using TLS connection 2020-12-13 21:30:33 +01:00
426cb33fab outposts: remove unused views 2020-12-13 21:25:05 +01:00
9e4f840d2d api: add token tests 2020-12-13 20:38:56 +01:00
e120d274e9 lib: fix sentry tests not running 2020-12-13 20:38:50 +01:00
977d3f6ef9 stages/user_write: add test that attributes without prefix are ignored 2020-12-13 20:38:43 +01:00
ecdbc917a5 admin: add api tests 2020-12-13 20:38:21 +01:00
0083cd55df sources/oauth: start adding tests for types 2020-12-13 20:03:34 +01:00
d380194e13 */saml: test against SAML Schema 2020-12-13 19:53:16 +01:00
32f5d5ba72 recovery: add test for invalid key 2020-12-13 18:46:36 +01:00
e818416863 policies/password: add invalid test case 2020-12-13 18:43:17 +01:00
7eed70cfe9 policies/hibp: add invalid test case 2020-12-13 18:42:59 +01:00
ea6ca23f57 lib: add tests for sentry integration 2020-12-13 18:41:47 +01:00
f056b026d6 lib: test edgecase for timedelta_from_string 2020-12-13 18:35:51 +01:00
1c0a6efeb1 flows/exporter: remove dead code since no stage is PolicyBindingModel 2020-12-13 18:25:30 +01:00
17732eea08 flows: add test for PLAN_CONTEXT_PENDING_USER_IDENTIFIER 2020-12-13 18:23:19 +01:00
aa5381fd59 flows: add tests case for reevaluate marker that keeps the stage 2020-12-13 18:07:11 +01:00
ffee86fcf3 crypto: simplify api/forms key validation 2020-12-13 18:06:52 +01:00
7ff7398aff admin: add tests for binding creation forms with invalid target 2020-12-13 18:06:34 +01:00
67925a39f2 web: fix source icons missing from static container 2020-12-13 17:50:30 +01:00
3b5e1c7b34 core: cleanup channels code, fix error when server side close 2020-12-13 17:46:34 +01:00
3e49acf7ae outposts: regularly ensure that all outposts have a valid service account and token 2020-12-13 17:10:56 +01:00
76764c4374 web: fix background for readonly inputs 2020-12-13 15:19:28 +01:00
9f6f8e1b55 outposts: update keys in outpost config 2020-12-13 15:15:20 +01:00
9590180c6c docs: update changelog 2020-12-13 12:41:42 +01:00
aef5c60a7b release: 0.13.0-rc3 2020-12-13 00:57:36 +01:00
d4c9c667c9 tests: fix URLs to use user-details instead of user-settings 2020-12-13 00:48:46 +01:00
96f0d582f0 core: load user detail form in an inner SiteShell so update doesn't reload entire page 2020-12-13 00:18:36 +01:00
7e8702a71e web: fix user detail form not working 2020-12-13 00:03:37 +01:00
1524061480 web: only auto-update slug when slug and name are already in sync 2020-12-12 23:45:47 +01:00
434922f702 web: make most client/network errors ignored by sentry 2020-12-12 23:32:55 +01:00
d2862ddc93 lifecycle: clean full redis as part of system migration 2020-12-12 23:30:49 +01:00
6e55431d4c stages/*: fix redirects not pointing to user_settings 2020-12-12 23:14:07 +01:00
01548c5e9c stages/*: fix links opening in SiteShell 2020-12-12 23:14:02 +01:00
bf1dae2dbe helm: make imagePullPolicy configurable 2020-12-12 23:13:58 +01:00
59c93defcf release: 0.13.0-rc2 2020-12-12 21:50:10 +01:00
a2a1a27502 web: fix icons not being included in static container 2020-12-12 21:49:00 +01:00
e3227e7d54 core: remove remaining references to old font 2020-12-12 21:41:12 +01:00
1f4a8fffdb docs: fix minor markdown and syntax errors 2020-12-12 21:30:05 +01:00
86b1183883 helm: bump version in readme 2020-12-12 21:27:05 +01:00
f781f4848c ci: fix release not depending on proxy build 2020-12-12 21:10:13 +01:00
19824d693c core: fix permission check for applications API 2020-12-12 21:00:35 +01:00
0694b911a4 docs: add changelog for 0.13 2020-12-12 21:00:23 +01:00
195 changed files with 4240 additions and 1367 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.13.0-rc1
current_version = 0.13.3-stable
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -23,6 +23,8 @@ values =
[bumpversion:file:helm/values.yaml]
[bumpversion:file:helm/README.md]
[bumpversion:file:helm/Chart.yaml]
[bumpversion:file:.github/workflows/release.yml]

View File

@ -18,11 +18,11 @@ jobs:
- name: Building Docker Image
run: docker build
--no-cache
-t beryju/authentik:0.13.0-rc1
-t beryju/authentik:0.13.3-stable
-t beryju/authentik:latest
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/authentik:0.13.0-rc1
run: docker push beryju/authentik:0.13.3-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/authentik:latest
build-proxy:
@ -48,11 +48,11 @@ jobs:
cd proxy/
docker build \
--no-cache \
-t beryju/authentik-proxy:0.13.0-rc1 \
-t beryju/authentik-proxy:0.13.3-stable \
-t beryju/authentik-proxy:latest \
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/authentik-proxy:0.13.0-rc1
run: docker push beryju/authentik-proxy:0.13.3-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/authentik-proxy:latest
build-static:
@ -69,17 +69,18 @@ jobs:
cd web/
docker build \
--no-cache \
-t beryju/authentik-static:0.13.0-rc1 \
-t beryju/authentik-static:0.13.3-stable \
-t beryju/authentik-static:latest \
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/authentik-static:0.13.0-rc1
run: docker push beryju/authentik-static:0.13.3-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/authentik-static:latest
test-release:
needs:
- build-server
- build-static
- build-proxy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
@ -106,5 +107,5 @@ jobs:
SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org
with:
tagName: 0.13.0-rc1
tagName: 0.13.3-stable
environment: beryjuorg-prod

View File

@ -38,6 +38,7 @@ RUN apt-get update && \
COPY ./authentik/ /authentik
COPY ./pytest.ini /
COPY ./xml /xml
COPY ./manage.py /
COPY ./lifecycle/ /lifecycle

View File

@ -1,5 +1,10 @@
all: lint-fix lint coverage gen
test-full:
coverage run manage.py test --failfast -v 3 .
coverage html
coverage report
test-integration:
k3d cluster create || exit 0
k3d kubeconfig write -o ~/.kube/config --overwrite

150
Pipfile.lock generated
View File

@ -53,10 +53,10 @@
},
"autobahn": {
"hashes": [
"sha256:24ce276d313e84d68241c3aef30d484f352b90a40168981b3640312c821df77b",
"sha256:86bbce30cdd407137c57670993a8f9bfdfe3f8e994b889181d85e844d5aa8dfb"
"sha256:491238c31f78721eaa9d0593909ab455a4ea68127aadd76ecf67185143f5f298",
"sha256:72b68a1ce1e10e3cbcc3b280aae86d5b2e7a1f409febab1ab91a8a3274113f6e"
],
"version": "==20.7.1"
"version": "==20.12.2"
},
"automat": {
"hashes": [
@ -74,18 +74,18 @@
},
"boto3": {
"hashes": [
"sha256:616cde1e326949020da85a5bacaa7ad287e9f117d10ac9c5bfb9150a98dfe1a7",
"sha256:ddad9ada00eccae1fc2da28c69531ba202fead562994ddcd9a9a232e993cd8a2"
"sha256:a05614300fd404c7952a55ae92e106b9400ae65886425aaab3104527be833848",
"sha256:c7556b0861d982b71043fbc0df024644320c817ad796391c442d0c2f15a77223"
],
"index": "pypi",
"version": "==1.16.34"
"version": "==1.16.39"
},
"botocore": {
"hashes": [
"sha256:49f5e56a7382a65ee0873371edcd91bdba8fc3f70abe102ebc1a0da2e6fbed06",
"sha256:4d81d92127ef646ae0f0ee84c9c220c92fa82312e765c29f8cb3b000fdbdd038"
"sha256:449e4196160ff58ee27d2a626a7ce4cfff2640fe1806d7a279e73a30ad286347",
"sha256:e0d0386098a072abd7b6c087e6149d997377c969a823ebe01b3f5bfabe9bfac0"
],
"version": "==1.19.34"
"version": "==1.19.39"
},
"cachetools": {
"hashes": [
@ -96,11 +96,11 @@
},
"celery": {
"hashes": [
"sha256:45bb7909061862305cefec94289fabc1b89ac004680f4dc7d9dea642a2507e53",
"sha256:533f3635065b7ed362ffc04228635b4c82d53a9ab812118ccdedb5eae281fb97"
"sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13",
"sha256:f4efebe6f8629b0da2b8e529424de376494f5b7a743c321c8a2ddc2b1414921c"
],
"index": "pypi",
"version": "==5.0.4"
"version": "==5.0.5"
},
"certifi": {
"hashes": [
@ -168,10 +168,10 @@
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
"version": "==3.0.4"
"version": "==4.0.0"
},
"click": {
"hashes": [
@ -343,11 +343,11 @@
},
"django-storages": {
"hashes": [
"sha256:12de8fb2605b9b57bfaf54b075280d7cbb3b3ee1ca4bc9b9add147af87fe3a2c",
"sha256:652275ab7844538c462b62810276c0244866f345878256a9e0e86f5b1283ae18"
"sha256:056ec3e9e2b0c6f363913976072ffba2923e79e4859578047da139ba1637497e",
"sha256:7af56611c62a1c174aab4e862efb7fdd98296dccf76f42135f5b6851fc313c97"
],
"index": "pypi",
"version": "==1.10.1"
"version": "==1.11"
},
"djangorestframework": {
"hashes": [
@ -396,10 +396,10 @@
},
"google-auth": {
"hashes": [
"sha256:5176db85f1e7e837a646cd9cede72c3c404ccf2e3373d9ee14b2db88febad440",
"sha256:b728625ff5dfce8f9e56a499c8a4eb51443a67f20f6d28b67d5774c310ec4b6b"
"sha256:0b0e026b412a0ad096e753907559e4bdb180d9ba9f68dd9036164db4fdc4ad2e",
"sha256:ce752cc51c31f479dbf9928435ef4b07514b20261b021c7383bee4bda646acb8"
],
"version": "==1.23.0"
"version": "==1.24.0"
},
"gunicorn": {
"hashes": [
@ -646,26 +646,46 @@
},
"msgpack": {
"hashes": [
"sha256:002a0d813e1f7b60da599bdf969e632074f9eec1b96cbed8fb0973a63160a408",
"sha256:25b3bc3190f3d9d965b818123b7752c5dfb953f0d774b454fd206c18fe384fb8",
"sha256:271b489499a43af001a2e42f42d876bb98ccaa7e20512ff37ca78c8e12e68f84",
"sha256:39c54fdebf5fa4dda733369012c59e7d085ebdfe35b6cf648f09d16708f1be5d",
"sha256:4233b7f86c1208190c78a525cd3828ca1623359ef48f78a6fea4b91bb995775a",
"sha256:5bea44181fc8e18eed1d0cd76e355073f00ce232ff9653a0ae88cb7d9e643322",
"sha256:5dba6d074fac9b24f29aaf1d2d032306c27f04187651511257e7831733293ec2",
"sha256:7a22c965588baeb07242cb561b63f309db27a07382825fc98aecaf0827c1538e",
"sha256:908944e3f038bca67fcfedb7845c4a257c7749bf9818632586b53bcf06ba4b97",
"sha256:9534d5cc480d4aff720233411a1f765be90885750b07df772380b34c10ecb5c0",
"sha256:aa5c057eab4f40ec47ea6f5a9825846be2ff6bf34102c560bad5cad5a677c5be",
"sha256:b3758dfd3423e358bbb18a7cccd1c74228dffa7a697e5be6cb9535de625c0dbf",
"sha256:c901e8058dd6653307906c5f157f26ed09eb94a850dddd989621098d347926ab",
"sha256:cec8bf10981ed70998d98431cd814db0ecf3384e6b113366e7f36af71a0fca08",
"sha256:db685187a415f51d6b937257474ca72199f393dad89534ebbdd7d7a3b000080e",
"sha256:e35b051077fc2f3ce12e7c6a34cf309680c63a842db3a0616ea6ed25ad20d272",
"sha256:e7bbdd8e2b277b77782f3ce34734b0dfde6cbe94ddb74de8d733d603c7f9e2b1",
"sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140"
"sha256:01835e300967e5ad6fdbfc36eafe74df67ff47e16e0d6dee8766630550315903",
"sha256:03c5554315317d76c25a15569dd52ac6047b105df71e861f24faf9675672b72d",
"sha256:0968b368a9a9081435bfcb7a57a1e8b75c7bf038ef911b369acd2e038c7f873a",
"sha256:1d7ab166401f7789bf11262439336c0a01b878f0d602e48f35c35d2e3a555820",
"sha256:1e8d27bac821f8aa909904a704a67e5e8bc2e42b153415fc3621b7afbc06702b",
"sha256:1fc9f21da9fd77088ebfd3c9941b044ca3f4a048e85f7ff5727f26bcdbffed61",
"sha256:20196229acc193939223118c7420838749d5b0cece49cd397739a3a6ffcfe2d1",
"sha256:2933443313289725f16bd7b99a8c3aa6a2cca1549e661d7407f056a0af80bf7b",
"sha256:2966b155356fd231fa441131d7301e1596ee38974ad56dc57fd752fdbe2bb63f",
"sha256:29a6fb3729215b6fcab786ef4f460a5406a5c056f7021191f70ff7712a3f6ba4",
"sha256:35cbefa7d7bddfb4b0770a1b9ff721cd8dfe9a680947a68457974d5e3e6acc2f",
"sha256:35ff1ac162a77fb78be360d9f771d36cbf1202e94fc6d70e284ad5db6ab72608",
"sha256:40dd1ac7420f071e96b3e4a4a7b8e69546a6f8065ff5995dbacf53f86207eb98",
"sha256:4bea1938e484c9caca9585105f447d6807c496c153b7244fa726b3cc4a68ec9e",
"sha256:4e58b9f4a99bc3a90859bb006ec4422448a5ce39e5cd6e7498c56de5dcec9c34",
"sha256:66d47e952856bfcde46d8351380d0b5b928a73112b66bc06d5367dfcc077c06a",
"sha256:69f6aa503378548ea1e760c11aeb6fc91952bf3634fd806a38a0e47edb507fcd",
"sha256:7033215267a0e9f60f4a5e4fb2228a932c404f237817caff8dc3115d9e7cd975",
"sha256:7b50afd767cc053ad92fad39947c3670db27305fd1c49acded44d9d9ac8b56fd",
"sha256:99ea9e65876546743b2b8bb5bc7adefbb03b9da78a899827467da197a48f790b",
"sha256:abcc62303ac4d789878d4aac4cdba1bbe2adb478d67be99cd4a6d56ac3a4028f",
"sha256:b107f9b36665bf7d7c6176a938a361a7aba16aa179d833919448f77287866484",
"sha256:b5b27923b6c98a2616b7e906a29e4e10e1b4424aea87a0e0d5636327dc6ea315",
"sha256:bf8eedc7bfbf63cbc9abe58287c32d78780a347835e82c23033c68f11f80bb05",
"sha256:c144ff4954a6ea40aa603600c8be175349588fc68696092889fa34ab6e055060",
"sha256:c4e5f96a1d0d916ce7a16decb7499e8923ddef007cf7d68412fb68767766648a",
"sha256:c60e8b2bf754b8dcc1075c5bee0b177ed9193e7cbd2377faaf507120a948e697",
"sha256:c82fc6cdba5737eb6ed0c926a30a5d56e7b050297375a16d6c5ad89b576fd979",
"sha256:ce4ebe2c79411cd5671b20862831880e7850a2de699cff6626f48853fde61ae6",
"sha256:d113c6b1239c62669ef3063693842605a3edbfebc39a333cf91ba60d314afe6d",
"sha256:d3cea07ad16919a44e8d1ea67efa5244855cdce807d672f41694acc24d08834e",
"sha256:d76672602db16e3f44bc1a85c7ee5f15d79e02fcf5bc9d133c2954753be6eddc",
"sha256:decf2091b75987ca2564e3b742f9614eb7d57e39ff04eaa68af7a3fc5648f7ed",
"sha256:e13b9007af66a3f62574bc0a13843df0e4402f5ee4b00a02aa1803f01d26b9fb",
"sha256:e157edf4213dacafb0f862e0b7a3a18448250cec91aa1334f432f49028acc650",
"sha256:e234ff83628ca3ab345bf97fb36ccbf6d2f1700f5e08868643bf4489edc960f8",
"sha256:f08d9dd3ce0c5e972dc4653f0fb66d2703941e65356388c13032b578dd718261",
"sha256:f20d7d4f1f0728560408ba6933154abccf0c20f24642a2404b43d5c23e4119ab"
],
"version": "==1.0.0"
"version": "==1.0.1"
},
"oauthlib": {
"hashes": [
@ -676,11 +696,11 @@
},
"packaging": {
"hashes": [
"sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236",
"sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"
"sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
"sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
],
"index": "pypi",
"version": "==20.7"
"version": "==20.8"
},
"prometheus-client": {
"hashes": [
@ -855,10 +875,10 @@
},
"pyopenssl": {
"hashes": [
"sha256:898aefbde331ba718570244c3b01dcddb1b31a3b336613436a45e52e27d9a82d",
"sha256:92f08eccbd73701cf744e8ffd6989aa7842d48cbe3fea8a7c031c5647f590ac5"
"sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51",
"sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b"
],
"version": "==20.0.0"
"version": "==20.0.1"
},
"pyparsing": {
"hashes": [
@ -930,10 +950,10 @@
},
"requests": {
"hashes": [
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
"version": "==2.25.0"
"version": "==2.25.1"
},
"requests-oauthlib": {
"hashes": [
@ -948,7 +968,7 @@
"sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa",
"sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233"
],
"markers": "python_version >= '3.5'",
"markers": "python_version >= '3.6'",
"version": "==4.6"
},
"ruamel.yaml": {
@ -1073,11 +1093,11 @@
"standard"
],
"hashes": [
"sha256:28420526640d800aabe648038f8e2ea8ba2a8bdc363002eecd5dfc57a0f75ab7",
"sha256:5123606e0f1d15ffbe0f63161c5078f7c28f350c5eb102435671eae58046db0f"
"sha256:2a7b17f4d9848d6557ccc2274a5f7c97f1daf037d130a0c6918f67cd9bc8cdf5",
"sha256:6fcce74c00b77d4f4b3ed7ba1b2a370d27133bfdb46f835b7a76dfe0a8c110ae"
],
"index": "pypi",
"version": "==0.13.0"
"version": "==0.13.1"
},
"uvloop": {
"hashes": [
@ -1263,11 +1283,11 @@
},
"bandit": {
"hashes": [
"sha256:2ff3fe35fe3212c0be5fc9c4899bd0108e2b5239c5ff62fb174639e4660fe958",
"sha256:d02dfe250f4aa2d166c127ad81d192579e2bfcdb8501717c0e2005e35a6bcf60"
"sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07",
"sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"
],
"index": "pypi",
"version": "==1.6.3"
"version": "==1.7.0"
},
"black": {
"hashes": [
@ -1453,11 +1473,11 @@
},
"packaging": {
"hashes": [
"sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236",
"sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"
"sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
"sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
],
"index": "pypi",
"version": "==20.7"
"version": "==20.8"
},
"pathspec": {
"hashes": [
@ -1496,10 +1516,10 @@
},
"py": {
"hashes": [
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
],
"version": "==1.9.0"
"version": "==1.10.0"
},
"pycodestyle": {
"hashes": [
@ -1566,11 +1586,11 @@
},
"pytest": {
"hashes": [
"sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe",
"sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"
"sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8",
"sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"
],
"index": "pypi",
"version": "==6.1.2"
"version": "==6.2.1"
},
"pytest-django": {
"hashes": [

View File

@ -1,4 +1,4 @@
<img src="icons/icon_top_brand.svg" height="250" alt="authentik logo">
<img src="web/icons/icon_top_brand.svg" height="250" alt="authentik logo">
---

View File

@ -6,9 +6,9 @@ As authentik is currently in a pre-stable, only the latest "stable" version is s
| Version | Supported |
| -------- | ------------------ |
| 0.10.x | :white_check_mark: |
| 0.11.x | :white_check_mark: |
| 0.12.x | :white_check_mark: |
| 0.13.x | :white_check_mark: |
## Reporting a Vulnerability

View File

@ -1,2 +1,2 @@
"""authentik"""
__version__ = "0.13.0-rc1"
__version__ = "0.13.3-stable"

View File

@ -1,4 +1,4 @@
"""authentik administration overview"""
"""authentik administration metrics"""
import time
from collections import Counter
from datetime import timedelta
@ -47,7 +47,7 @@ def get_events_per_1h(**filter_kwargs) -> List[Dict[str, int]]:
class AdministrationMetricsSerializer(Serializer):
"""Overview View"""
"""Login Metrics per 1h"""
logins_per_1h = SerializerMethodField()
logins_failed_per_1h = SerializerMethodField()
@ -68,12 +68,12 @@ class AdministrationMetricsSerializer(Serializer):
class AdministrationMetricsViewSet(ViewSet):
"""Return single instance of AdministrationMetricsSerializer"""
"""Login Metrics per 1h"""
permission_classes = [IsAdminUser]
@swagger_auto_schema(responses={200: AdministrationMetricsSerializer(many=True)})
def list(self, request: Request) -> Response:
"""Return single instance of AdministrationMetricsSerializer"""
"""Login Metrics per 1h"""
serializer = AdministrationMetricsSerializer(True)
return Response(serializer.data)

View File

@ -1,79 +0,0 @@
"""authentik administration overview"""
from django.core.cache import cache
from drf_yasg2.utils import swagger_auto_schema
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.serializers import Serializer
from rest_framework.viewsets import ViewSet
from authentik import __version__
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
from authentik.core.models import Provider
from authentik.policies.models import Policy
from authentik.root.celery import CELERY_APP
class AdministrationOverviewSerializer(Serializer):
"""Overview View"""
version = SerializerMethodField()
version_latest = SerializerMethodField()
worker_count = SerializerMethodField()
providers_without_application = SerializerMethodField()
policies_without_binding = SerializerMethodField()
cached_policies = SerializerMethodField()
cached_flows = SerializerMethodField()
def get_version(self, _) -> str:
"""Get current version"""
return __version__
def get_version_latest(self, _) -> str:
"""Get latest version from cache"""
version_in_cache = cache.get(VERSION_CACHE_KEY)
if not version_in_cache:
update_latest_version.delay()
return __version__
return version_in_cache
def get_worker_count(self, _) -> int:
"""Ping workers"""
return len(CELERY_APP.control.ping(timeout=0.5))
def get_providers_without_application(self, _) -> int:
"""Count of providers without application"""
return len(Provider.objects.filter(application=None))
def get_policies_without_binding(self, _) -> int:
"""Count of policies not bound or use in prompt stages"""
return len(
Policy.objects.filter(bindings__isnull=True, promptstage__isnull=True)
)
def get_cached_policies(self, _) -> int:
"""Get cached policy count"""
return len(cache.keys("policy_*"))
def get_cached_flows(self, _) -> int:
"""Get cached flow count"""
return len(cache.keys("flow_*"))
def create(self, request: Request) -> Response:
raise NotImplementedError
def update(self, request: Request) -> Response:
raise NotImplementedError
class AdministrationOverviewViewSet(ViewSet):
"""Return single instance of AdministrationOverviewSerializer"""
permission_classes = [IsAdminUser]
@swagger_auto_schema(responses={200: AdministrationOverviewSerializer(many=True)})
def list(self, request: Request) -> Response:
"""Return single instance of AdministrationOverviewSerializer"""
serializer = AdministrationOverviewSerializer(True)
return Response(serializer.data)

View File

@ -66,7 +66,7 @@ class TaskViewSet(ViewSet):
"successful": True,
}
)
except ImportError:
except ImportError: # pragma: no cover
# if we get an import error, the module path has probably changed
task.delete()
return Response({"successful": False})

View File

@ -0,0 +1,60 @@
"""authentik administration overview"""
from django.core.cache import cache
from drf_yasg2.utils import swagger_auto_schema
from packaging.version import parse
from rest_framework.fields import SerializerMethodField
from rest_framework.mixins import ListModelMixin
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 authentik import __version__
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
class VersionSerializer(Serializer):
"""Get running and latest version."""
version_current = SerializerMethodField()
version_latest = SerializerMethodField()
outdated = SerializerMethodField()
def get_version_current(self, _) -> str:
"""Get current version"""
return __version__
def get_version_latest(self, _) -> str:
"""Get latest version from cache"""
version_in_cache = cache.get(VERSION_CACHE_KEY)
if not version_in_cache: # pragma: no cover
update_latest_version.delay()
return __version__
return version_in_cache
def get_outdated(self, instance) -> bool:
"""Check if we're running the latest version"""
return parse(self.get_version_current(instance)) < parse(
self.get_version_latest(instance)
)
def create(self, request: Request) -> Response:
raise NotImplementedError
def update(self, request: Request) -> Response:
raise NotImplementedError
class VersionViewSet(ListModelMixin, GenericViewSet):
"""Get running and latest version."""
permission_classes = [IsAdminUser]
def get_queryset(self):
return None
@swagger_auto_schema(responses={200: VersionSerializer(many=True)})
def list(self, request: Request) -> Response:
"""Get running and latest version."""
return Response(VersionSerializer(True).data)

View File

@ -0,0 +1,25 @@
"""authentik administration overview"""
from rest_framework.mixins import ListModelMixin
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 authentik.root.celery import CELERY_APP
class WorkerViewSet(ListModelMixin, GenericViewSet):
"""Get currently connected worker count."""
serializer_class = Serializer
permission_classes = [IsAdminUser]
def get_queryset(self):
return None
def list(self, request: Request) -> Response:
"""Get currently connected worker count."""
return Response(
{"pagination": {"count": len(CELERY_APP.control.ping(timeout=0.5))}}
)

View File

@ -1,230 +0,0 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load static %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>{% trans 'System Overview' %}</h1>
</div>
</section>
<section class="pf-c-page__main-section">
<div class="pf-l-gallery pf-m-gutter">
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 3;grid-row-end: span 2;">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Logins over the last 24 hours' %}
</div>
</div>
<div class="pf-c-card__body">
<ak-admin-logins-chart url="{% url 'authentik_api:admin_metrics-list' %}"></ak-admin-logins-chart>
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 2;grid-row-end: span 3;">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Apps with most usage' %}
</div>
</div>
<div class="pf-c-card__body">
<table class="pf-c-table pf-m-compact" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Application' %}</th>
<th role="columnheader" scope="col">{% trans 'Logins' %}</th>
<th role="columnheader" scope="col"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for app in most_used_applications %}
<tr role="row">
<td role="cell">
{{ app.application.name }}
</td>
<td role="cell">
{{ app.total_logins }}
</td>
<td role="cell">
<progress value="{{ app.total_logins }}" max="{{ most_used_applications.0.total_logins }}"></progress>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
</div>
<a href="{% url 'authentik_admin:providers' %}">
<i class="fa fa-external-link-alt"> </i>
</a>
</div>
<div class="pf-c-card__body">
{% if providers_without_application.exists %}
<p class="ak-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> {{ provider_count }}
</p>
<p>{% trans 'Warning: At least one Provider has no application assigned.' %}</p>
{% else %}
<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> {{ provider_count }}
</p>
{% endif %}
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
</div>
<a href="{% url 'authentik_admin:policies' %}">
<i class="fa fa-external-link-alt"> </i>
</a>
</div>
<div class="pf-c-card__body">
{% if policies_without_binding %}
<p class="ak-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> {{ policy_count }}
</p>
<p>{% trans 'Policies without binding exist.' %}</p>
{% else %}
<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> {{ policy_count }}
</p>
{% endif %}
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
</div>
<a href="{% url 'authentik_admin:users' %}">
<i class="fa fa-external-link-alt"> </i>
</a>
</div>
<div class="pf-c-card__body">
<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> {{ user_count }}
</p>
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
</div>
<a href="https://github.com/BeryJu/authentik/releases" target="_blank">
<i class="fa fa-external-link-alt"> </i>
</a>
</div>
<div class="pf-c-card__body">
<p class="ak-aggregate-card">
{% if version >= version_latest %}
<i class="fa fa-check-circle"></i> {{ version }}
{% else %}
<i class="fa fa-exclamation-triangle"></i> {{ version }}
{% endif %}
</p>
{% if version >= version_latest %}
{% blocktrans %}
Up-to-date!
{% endblocktrans %}
{% else %}
{% blocktrans with latest=version_latest %}
{{ latest }} is available!
{% endblocktrans %}
{% endif %}
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
</div>
</div>
<fetch-fill-slot class="pf-c-card__body" url="{% url 'authentik_api:admin_overview-list' %}" key="worker_count">
<div slot="value < 1">
<p class="ak-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> <span data-value></span>
</p>
<p>{% trans 'No workers connected.' %}</p>
</div>
<div slot="value >= 1">
<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> <span data-value></span>
</p>
</div>
<div>
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
</fetch-fill-slot>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
</div>
<ak-modal-button href="{% url 'authentik_admin:overview-clear-policy-cache' %}">
<a slot="trigger">
<i class="fa fa-trash"> </i>
</a>
<div slot="modal"></div>
</ak-modal-button>
</div>
<div class="pf-c-card__body">
{% if cached_policies < 1 %}
<p class="ak-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> {{ cached_policies }}
</p>
<p>{% trans 'No policies cached. Users may experience slow response times.' %}</p>
{% else %}
<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> {{ cached_policies }}
</p>
{% endif %}
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %}
</div>
<ak-modal-button href="{% url 'authentik_admin:overview-clear-flow-cache' %}">
<a slot="trigger">
<i class="fa fa-trash"> </i>
</a>
<div slot="modal"></div>
</ak-modal-button>
</div>
<div class="pf-c-card__body">
{% if cached_flows < 1 %}
<p class="ak-aggregate-card">
<span class="fa fa-exclamation-triangle"></span> {{ cached_flows }}
</p>
<p>{% trans 'No flows cached.' %}</p>
{% else %}
<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> {{ cached_flows }}
</p>
{% endif %}
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,73 @@
"""test admin api"""
from json import loads
from django.shortcuts import reverse
from django.test import TestCase
from authentik import __version__
from authentik.core.models import Group, User
from authentik.core.tasks import clean_expired_models
class TestAdminAPI(TestCase):
"""test admin api"""
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create(username="test-user")
self.group = Group.objects.create(name="superusers", is_superuser=True)
self.group.users.add(self.user)
self.group.save()
self.client.force_login(self.user)
def test_tasks(self):
"""Test Task API"""
clean_expired_models.delay()
response = self.client.get(reverse("authentik_api:admin_system_tasks-list"))
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertTrue(
any([task["task_name"] == "clean_expired_models" for task in body])
)
def test_tasks_retry(self):
"""Test Task API (retry)"""
clean_expired_models.delay()
response = self.client.post(
reverse(
"authentik_api:admin_system_tasks-retry",
kwargs={"pk": "clean_expired_models"},
)
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertTrue(body["successful"])
def test_tasks_retry_404(self):
"""Test Task API (retry, 404)"""
response = self.client.post(
reverse(
"authentik_api:admin_system_tasks-retry",
kwargs={"pk": "qwerqewrqrqewrqewr"},
)
)
self.assertEqual(response.status_code, 404)
def test_version(self):
"""Test Version API"""
response = self.client.get(reverse("authentik_api:admin_version-list"))
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"))
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(body["pagination"]["count"], 0)
def test_metrics(self):
"""Test metrics API"""
response = self.client.get(reverse("authentik_api:admin_metrics-list"))
self.assertEqual(response.status_code, 200)

View File

@ -1,9 +1,13 @@
"""admin tests"""
from uuid import uuid4
from django import forms
from django.test import TestCase
from django.test.client import RequestFactory
from authentik.admin.views.policies_bindings import PolicyBindingCreateView
from authentik.core.models import Application
from authentik.policies.forms import PolicyBindingForm
class TestPolicyBindingView(TestCase):
@ -18,9 +22,22 @@ class TestPolicyBindingView(TestCase):
view = PolicyBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {})
def test_with_param(self):
def test_with_params_invalid(self):
"""Test PolicyBindingCreateView with invalid get params"""
request = self.factory.get("/", {"target": uuid4()})
view = PolicyBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {})
def test_with_params(self):
"""Test PolicyBindingCreateView with get params"""
target = Application.objects.create(name="test")
request = self.factory.get("/", {"target": target.pk.hex})
view = PolicyBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {"target": target, "order": 0})
self.assertTrue(
isinstance(
PolicyBindingForm(initial={"target": "foo"}).fields["target"].widget,
forms.HiddenInput,
)
)

View File

@ -1,8 +1,12 @@
"""admin tests"""
from uuid import uuid4
from django import forms
from django.test import TestCase
from django.test.client import RequestFactory
from authentik.admin.views.stages_bindings import StageBindingCreateView
from authentik.flows.forms import FlowStageBindingForm
from authentik.flows.models import Flow
@ -18,9 +22,22 @@ class TestStageBindingView(TestCase):
view = StageBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {})
def test_with_param(self):
def test_with_params_invalid(self):
"""Test StageBindingCreateView with invalid get params"""
request = self.factory.get("/", {"target": uuid4()})
view = StageBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {})
def test_with_params(self):
"""Test StageBindingCreateView with get params"""
target = Flow.objects.create(name="test", slug="test")
request = self.factory.get("/", {"target": target.pk.hex})
view = StageBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {"target": target, "order": 0})
self.assertTrue(
isinstance(
FlowStageBindingForm(initial={"target": "foo"}).fields["target"].widget,
forms.HiddenInput,
)
)

View File

@ -34,7 +34,6 @@ urlpatterns = [
overview.PolicyCacheClearView.as_view(),
name="overview-clear-policy-cache",
),
path("overview/", overview.AdministrationOverviewView.as_view(), name="overview"),
# Applications
path(
"applications/", applications.ApplicationListView.as_view(), name="applications"

View File

@ -1,65 +1,25 @@
"""authentik administration overview"""
from typing import Union
from django.conf import settings
from django.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import FormView, TemplateView
from packaging.version import LegacyVersion, Version, parse
from django.views.generic import FormView
from structlog import get_logger
from authentik import __version__
from authentik.admin.forms.overview import FlowCacheClearForm, PolicyCacheClearForm
from authentik.admin.mixins import AdminRequiredMixin
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
from authentik.core.models import Provider, User
from authentik.policies.models import Policy
LOGGER = get_logger()
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
"""Overview View"""
template_name = "administration/overview.html"
def get_latest_version(self) -> Union[LegacyVersion, Version]:
"""Get latest version from cache"""
version_in_cache = cache.get(VERSION_CACHE_KEY)
if not version_in_cache:
if not settings.DEBUG:
update_latest_version.delay()
return parse(__version__)
return parse(version_in_cache)
def get_context_data(self, **kwargs):
kwargs["policy_count"] = len(Policy.objects.all())
kwargs["user_count"] = len(User.objects.all()) - 1 # Remove anonymous user
kwargs["provider_count"] = len(Provider.objects.all())
kwargs["version"] = parse(__version__)
kwargs["version_latest"] = self.get_latest_version()
kwargs["providers_without_application"] = Provider.objects.filter(
application=None
)
kwargs["policies_without_binding"] = len(
Policy.objects.filter(bindings__isnull=True, promptstage__isnull=True)
)
kwargs["cached_policies"] = len(cache.keys("policy_*"))
kwargs["cached_flows"] = len(cache.keys("flow_*"))
return super().get_context_data(**kwargs)
class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
"""View to clear Policy cache"""
form_class = PolicyCacheClearForm
template_name = "generic/form_non_model.html"
success_url = reverse_lazy("authentik_admin:overview")
success_url = "/"
success_message = _("Successfully cleared Policy cache")
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
@ -75,7 +35,7 @@ class FlowCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
form_class = FlowCacheClearForm
template_name = "generic/form_non_model.html"
success_url = reverse_lazy("authentik_admin:overview")
success_url = "/"
success_message = _("Successfully cleared Flow cache")
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:

View File

@ -1,5 +1,6 @@
"""API Authentication"""
from base64 import b64decode
from binascii import Error
from typing import Any, Optional, Tuple, Union
from rest_framework.authentication import BaseAuthentication, get_authorization_header
@ -24,7 +25,7 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
return None
try:
auth_credentials = b64decode(auth_credentials.encode()).decode()
except UnicodeDecodeError:
except (UnicodeDecodeError, Error):
return None
# Accept credentials with username and without
if ":" in auth_credentials:

37
authentik/api/tests.py Normal file
View File

@ -0,0 +1,37 @@
"""Test API Authentication"""
from base64 import b64encode
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.api.auth import token_from_header
from authentik.core.models import Token, TokenIntents
class TestAPIAuth(TestCase):
"""Test API Authentication"""
def test_valid(self):
"""Test valid token"""
token = Token.objects.create(
intent=TokenIntents.INTENT_API, user=get_anonymous_user()
)
auth = b64encode(f":{token.key}".encode()).decode()
self.assertEqual(token_from_header(f"Basic {auth}".encode()), token)
def test_invalid_type(self):
"""Test invalid type"""
self.assertIsNone(token_from_header("foo bar".encode()))
def test_invalid_decode(self):
"""Test invalid bas64"""
self.assertIsNone(token_from_header("Basic bar".encode()))
def test_invalid_empty_password(self):
"""Test invalid with empty password"""
self.assertIsNone(token_from_header("Basic :".encode()))
def test_invalid_no_token(self):
"""Test invalid with no token"""
auth = b64encode(":abc".encode()).decode()
self.assertIsNone(token_from_header(f"Basic :{auth}".encode()))

View File

@ -5,9 +5,10 @@ from drf_yasg2.views import get_schema_view
from rest_framework import routers
from rest_framework.permissions import AllowAny
from authentik.admin.api.overview import AdministrationOverviewViewSet
from authentik.admin.api.overview_metrics import AdministrationMetricsViewSet
from authentik.admin.api.metrics import AdministrationMetricsViewSet
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.v2.messages import MessagesViewSet
from authentik.audit.api import EventViewSet
@ -19,13 +20,22 @@ from authentik.core.api.sources import SourceViewSet
from authentik.core.api.tokens import TokenViewSet
from authentik.core.api.users import UserViewSet
from authentik.crypto.api import CertificateKeyPairViewSet
from authentik.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
from authentik.flows.api import (
FlowCacheViewSet,
FlowStageBindingViewSet,
FlowViewSet,
StageViewSet,
)
from authentik.outposts.api import (
DockerServiceConnectionViewSet,
KubernetesServiceConnectionViewSet,
OutpostViewSet,
)
from authentik.policies.api import PolicyBindingViewSet, PolicyViewSet
from authentik.policies.api import (
PolicyBindingViewSet,
PolicyCacheViewSet,
PolicyViewSet,
)
from authentik.policies.dummy.api import DummyPolicyViewSet
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
from authentik.policies.expression.api import ExpressionPolicyViewSet
@ -63,9 +73,8 @@ router = routers.DefaultRouter()
router.register("root/messages", MessagesViewSet, basename="messages")
router.register("root/config", ConfigsViewSet, basename="configs")
router.register(
"admin/overview", AdministrationOverviewViewSet, basename="admin_overview"
)
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")
@ -82,6 +91,7 @@ router.register(
router.register("outposts/proxy", ProxyOutpostConfigViewSet)
router.register("flows/instances", FlowViewSet)
router.register("flows/cached", FlowCacheViewSet, basename="flows_cache")
router.register("flows/bindings", FlowStageBindingViewSet)
router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
@ -94,6 +104,7 @@ router.register("sources/saml", SAMLSourceViewSet)
router.register("sources/oauth", OAuthSourceViewSet)
router.register("policies/all", PolicyViewSet)
router.register("policies/cached", PolicyCacheViewSet, basename="policies_cache")
router.register("policies/bindings", PolicyBindingViewSet)
router.register("policies/expression", ExpressionPolicyViewSet)
router.register("policies/group_membership", GroupMembershipPolicyViewSet)

View File

@ -1,14 +1,17 @@
"""Application API Views"""
from django.db.models import QuerySet
from django.http.response import Http404
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import SerializerMethodField
from rest_framework.generics import get_object_or_404
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.admin.api.overview_metrics import get_events_per_1h
from authentik.admin.api.metrics import get_events_per_1h
from authentik.audit.models import EventAction
from authentik.core.models import Application
from authentik.policies.engine import PolicyEngine
@ -71,8 +74,12 @@ class ApplicationViewSet(ModelViewSet):
@action(detail=True)
def metrics(self, request: Request, slug: str):
"""Metrics for application logins"""
# TODO: Check app read and audit read perms
app = Application.objects.get(slug=slug)
app = get_object_or_404(
get_objects_for_user(request.user, "authentik_core.view_application"),
slug=slug,
)
if not request.user.has_perm("authentik_audit.view_event"):
raise Http404
return Response(
get_events_per_1h(
action=EventAction.AUTHORIZE_APPLICATION,

View File

@ -1,6 +1,6 @@
"""Provider API Views"""
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.viewsets import ModelViewSet
from authentik.core.models import Provider
@ -14,17 +14,33 @@ class ProviderSerializer(ModelSerializer):
"""Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace("provider", "")
def to_representation(self, instance: Provider):
# pyright: reportGeneralTypeIssues=false
if instance.__class__ == Provider:
return super().to_representation(instance)
return instance.serializer(instance=instance).data
class Meta:
model = Provider
fields = ["pk", "name", "authorization_flow", "property_mappings", "__type__"]
fields = [
"pk",
"name",
"application",
"authorization_flow",
"property_mappings",
"__type__",
]
class ProviderViewSet(ReadOnlyModelViewSet):
class ProviderViewSet(ModelViewSet):
"""Provider Viewset"""
queryset = Provider.objects.all()
serializer_class = ProviderSerializer
filterset_fields = {
"application": ["isnull"],
}
def get_queryset(self):
return Provider.objects.select_subclasses()

View File

@ -1,4 +1,5 @@
"""Channels base classes"""
from channels.exceptions import DenyConnection
from channels.generic.websocket import JsonWebsocketConsumer
from structlog import get_logger
@ -17,16 +18,13 @@ class AuthJsonConsumer(JsonWebsocketConsumer):
headers = dict(self.scope["headers"])
if b"authorization" not in headers:
LOGGER.warning("WS Request without authorization header")
self.close()
return False
raise DenyConnection()
raw_header = headers[b"authorization"]
token = token_from_header(raw_header)
if not token:
LOGGER.warning("Failed to authenticate")
self.close()
return False
raise DenyConnection()
self.user = token.user
return True

View File

@ -14,6 +14,7 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from guardian.mixins import GuardianUserMixin
from model_utils.managers import InheritanceManager
from rest_framework.serializers import Serializer
from structlog import get_logger
from authentik.core.exceptions import PropertyMappingExpressionException
@ -127,7 +128,7 @@ class User(GuardianUserMixin, AbstractUser):
verbose_name_plural = _("Users")
class Provider(models.Model):
class Provider(SerializerModel):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
name = models.TextField()
@ -156,6 +157,11 @@ class Provider(models.Model):
"""Return Form class used to edit this object"""
raise NotImplementedError
@property
def serializer(self) -> Type[Serializer]:
"""Get serializer for this model"""
raise NotImplementedError
def __str__(self):
return self.name

View File

@ -6,8 +6,6 @@
<html lang="en">
<head>
<link rel="preload" href="{% static 'dist/assets/fonts/DINEngschriftStd.woff2' %}" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="{% static 'dist/assets/fonts/DINEngschriftStd.woff' %}" as="font" type="font/woff" crossorigin>
<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>

View File

@ -3,6 +3,15 @@
{% load i18n %}
{% load authentik_utils %}
{% block head %}
{{ block.super }}
<style>
.pf-c-empty-state {
height: 100vh;
}
</style>
{% endblock %}
{% block body %}
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
<div class="pf-c-empty-state">

View File

@ -0,0 +1,26 @@
{% load i18n %}
<div class="pf-c-card">
<div class="pf-c-card__header pf-c-title pf-m-md">
{% trans 'Update details' %}
</div>
<div class="pf-c-card__body">
<form action="" method="post" class="pf-c-form pf-m-horizontal">
{% include 'partials/form_horizontal.html' with form=form %}
{% block beneath_form %}
{% endblock %}
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__horizontal-group">
<div class="pf-c-form__actions">
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
{% if unenrollment_enabled %}
<a class="pf-c-button pf-m-danger"
href="{% url 'authentik_flows:default-unenrollment' %}?back={{ request.get_full_path }}">{%
trans "Delete account" %}</a>
{% endif %}
</div>
</div>
</div>
</form>
</div>
</div>

View File

@ -15,29 +15,9 @@
<section class="pf-c-page__main-section">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
<div class="pf-c-card">
<div class="pf-c-card__header pf-c-title pf-m-md">
{% trans 'Update details' %}
</div>
<div class="pf-c-card__body">
<form action="" method="post" class="pf-c-form pf-m-horizontal">
{% include 'partials/form_horizontal.html' with form=form %}
{% block beneath_form %}
{% endblock %}
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__horizontal-group">
<div class="pf-c-form__actions">
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
{% if unenrollment_enabled %}
<a class="pf-c-button pf-m-danger"
href="{% url 'authentik_flows:default-unenrollment' %}?back={{ request.get_full_path }}">{% trans "Delete account" %}</a>
{% endif %}
</div>
</div>
</div>
</form>
</div>
</div>
<ak-site-shell url="{% url 'authentik_core:user-details' %}">
<div slot="body"></div>
</ak-site-shell>
</div>
</div>
</section>

View File

@ -34,9 +34,3 @@ class TestOverviewViews(TestCase):
self.assertEqual(
self.client.get(reverse("authentik_core:overview")).status_code, 200
)
def test_user_settings(self):
"""Test user settings"""
self.assertEqual(
self.client.get(reverse("authentik_core:user-settings")).status_code, 200
)

View File

@ -28,3 +28,9 @@ class TestUserViews(TestCase):
self.assertEqual(
self.client.get(reverse("authentik_core:user-settings")).status_code, 200
)
def test_user_details(self):
"""Test UserDetailsView"""
self.assertEqual(
self.client.get(reverse("authentik_core:user-details")).status_code, 200
)

View File

@ -7,6 +7,7 @@ urlpatterns = [
path("", shell.ShellView.as_view(), name="shell"),
# User views
path("-/user/", user.UserSettingsView.as_view(), name="user-settings"),
path("-/user/details/", user.UserDetailsView.as_view(), name="user-details"),
path("-/user/tokens/", user.TokenListView.as_view(), name="user-tokens"),
path(
"-/user/tokens/create/",

View File

@ -11,6 +11,7 @@ from django.http.response import HttpResponse
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView
from django.views.generic.base import TemplateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from guardian.shortcuts import get_objects_for_user
@ -26,14 +27,20 @@ from authentik.flows.models import Flow, FlowDesignation
from authentik.lib.views import CreateAssignPermView
class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
"""Update User settings"""
class UserSettingsView(TemplateView):
"""Multiple SiteShells for user details and all stages"""
template_name = "user/settings.html"
class UserDetailsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
"""Update User details"""
template_name = "user/details.html"
form_class = UserDetailForm
success_message = _("Successfully updated user.")
success_url = reverse_lazy("authentik_core:user-settings")
success_url = reverse_lazy("authentik_core:user-details")
def get_object(self):
return self.request.user

View File

@ -22,16 +22,15 @@ class CertificateKeyPairSerializer(ModelSerializer):
def validate_key_data(self, value):
"""Verify that input is a valid PEM RSA Key"""
# Since this field is optional, data can be empty.
if value == "":
return value
try:
load_pem_private_key(
str.encode("\n".join([x.strip() for x in value.split("\n")])),
password=None,
backend=default_backend(),
)
except ValueError:
raise ValidationError("Unable to load private key.")
if value != "":
try:
load_pem_private_key(
str.encode("\n".join([x.strip() for x in value.split("\n")])),
password=None,
backend=default_backend(),
)
except ValueError:
raise ValidationError("Unable to load private key.")
return value
class Meta:

View File

@ -26,16 +26,15 @@ class CertificateKeyPairForm(forms.ModelForm):
"""Verify that input is a valid PEM RSA Key"""
key_data = self.cleaned_data["key_data"]
# Since this field is optional, data can be empty.
if key_data == "":
return key_data
try:
load_pem_private_key(
str.encode("\n".join([x.strip() for x in key_data.split("\n")])),
password=None,
backend=default_backend(),
)
except ValueError:
raise forms.ValidationError("Unable to load private key.")
if key_data != "":
try:
load_pem_private_key(
str.encode("\n".join([x.strip() for x in key_data.split("\n")])),
password=None,
backend=default_backend(),
)
except ValueError:
raise forms.ValidationError("Unable to load private key.")
return key_data
class Meta:

View File

@ -1,7 +1,14 @@
"""Flow API Views"""
from django.core.cache import cache
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.mixins import ListModelMixin
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import (
ModelSerializer,
Serializer,
SerializerMethodField,
)
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.flows.models import Flow, FlowStageBinding, Stage
from authentik.flows.planner import cache_key
@ -98,3 +105,14 @@ class FlowStageBindingViewSet(ModelViewSet):
queryset = FlowStageBinding.objects.all()
serializer_class = FlowStageBindingSerializer
filterset_fields = "__all__"
class FlowCacheViewSet(ListModelMixin, GenericViewSet):
"""Info about cached flows"""
queryset = Flow.objects.none()
serializer_class = Serializer
def list(self, request: Request) -> Response:
"""Info about cached flows"""
return Response(data={"pagination": {"count": len(cache.keys("flow_*"))}})

View File

@ -19,6 +19,7 @@ LOGGER = get_logger()
PLAN_CONTEXT_PENDING_USER = "pending_user"
PLAN_CONTEXT_SSO = "is_sso"
PLAN_CONTEXT_REDIRECT = "redirect"
PLAN_CONTEXT_APPLICATION = "application"

View File

@ -3,14 +3,17 @@ from unittest.mock import MagicMock, PropertyMock, patch
from django.http import HttpRequest, HttpResponse
from django.shortcuts import reverse
from django.test import Client, TestCase
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils.encoding import force_str
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.core.models import User
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import FlowPlan
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN
from authentik.flows.planner import FlowPlan, FlowPlanner
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView
from authentik.lib.config import CONFIG
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.http import AccessDeniedResponse
@ -35,8 +38,12 @@ class TestFlowExecutor(TestCase):
"""Test views logic"""
def setUp(self):
self.client = Client()
self.request_factory = RequestFactory()
@patch(
"authentik.flows.views.to_stage_response",
TO_STAGE_RESPONSE_MOCK,
)
def test_existing_plan_diff_flow(self):
"""Check that a plan for a different flow cancels the current plan"""
flow = Flow.objects.create(
@ -59,7 +66,7 @@ class TestFlowExecutor(TestCase):
"authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}
),
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, 302)
self.assertEqual(cancel_mock.call_count, 2)
@patch(
@ -102,10 +109,13 @@ class TestFlowExecutor(TestCase):
response = self.client.get(
reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
)
self.assertEqual(response.status_code, 200)
self.assertIsInstance(response, AccessDeniedResponse)
self.assertInHTML(EmptyFlowException.__doc__, response.rendered_content)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("authentik_core:shell"))
@patch(
"authentik.flows.views.to_stage_response",
TO_STAGE_RESPONSE_MOCK,
)
def test_invalid_flow_redirect(self):
"""Tests that an invalid flow still redirects"""
flow = Flow.objects.create(
@ -118,11 +128,8 @@ class TestFlowExecutor(TestCase):
dest = "/unique-string"
url = reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug})
response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}")
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"type": "redirect", "to": dest},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("authentik_core:shell"))
def test_multi_stage_flow(self):
"""Test a full flow with multiple stages"""
@ -158,6 +165,10 @@ class TestFlowExecutor(TestCase):
plan: FlowPlan = session[SESSION_KEY_PLAN]
self.assertEqual(len(plan.stages), 1)
@patch(
"authentik.flows.views.to_stage_response",
TO_STAGE_RESPONSE_MOCK,
)
def test_reevaluate_remove_last(self):
"""Test planner with re-evaluate (last stage is removed)"""
flow = Flow.objects.create(
@ -276,6 +287,83 @@ class TestFlowExecutor(TestCase):
{"type": "redirect", "to": reverse("authentik_core:shell")},
)
def test_reevaluate_keep(self):
"""Test planner with re-evaluate (everything is kept)"""
flow = Flow.objects.create(
name="test-default-context",
slug="test-default-context",
designation=FlowDesignation.AUTHENTICATION,
)
true_policy = DummyPolicy.objects.create(result=True, wait_min=1, wait_max=2)
binding = FlowStageBinding.objects.create(
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
)
binding2 = FlowStageBinding.objects.create(
target=flow,
stage=DummyStage.objects.create(name="dummy2"),
order=1,
re_evaluate_policies=True,
)
binding3 = FlowStageBinding.objects.create(
target=flow, stage=DummyStage.objects.create(name="dummy3"), order=2
)
PolicyBinding.objects.create(policy=true_policy, target=binding2, order=0)
# Here we patch the dummy policy to evaluate to true so the stage is included
with patch(
"authentik.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE
):
exec_url = reverse(
"authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}
)
# First request, run the planner
response = self.client.get(exec_url)
self.assertEqual(response.status_code, 200)
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding.stage)
self.assertEqual(plan.stages[1], binding2.stage)
self.assertEqual(plan.stages[2], binding3.stage)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
self.assertIsInstance(plan.markers[2], StageMarker)
# Second request, this passes the first dummy stage
response = self.client.post(exec_url)
self.assertEqual(response.status_code, 302)
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding2.stage)
self.assertEqual(plan.stages[1], binding3.stage)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], StageMarker)
# Third request, this passes the first dummy stage
response = self.client.post(exec_url)
self.assertEqual(response.status_code, 302)
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding3.stage)
self.assertIsInstance(plan.markers[0], StageMarker)
# third request, this should trigger the re-evaluate
# We do this request without the patch, so the policy results in false
response = self.client.post(exec_url)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"type": "redirect", "to": reverse("authentik_core:shell")},
)
def test_reevaluate_remove_consecutive(self):
"""Test planner with re-evaluate (consecutive stages are removed)"""
flow = Flow.objects.create(
@ -351,3 +439,33 @@ class TestFlowExecutor(TestCase):
force_str(response.content),
{"type": "redirect", "to": reverse("authentik_core:shell")},
)
def test_stageview_user_identifier(self):
"""Test PLAN_CONTEXT_PENDING_USER_IDENTIFIER"""
flow = Flow.objects.create(
name="test-default-context",
slug="test-default-context",
designation=FlowDesignation.AUTHENTICATION,
)
FlowStageBinding.objects.create(
target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
)
ident = "test-identifier"
user = User.objects.create(username="test-user")
request = self.request_factory.get(
reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
)
request.user = user
planner = FlowPlanner(flow)
plan = planner.plan(
request, default_context={PLAN_CONTEXT_PENDING_USER_IDENTIFIER: ident}
)
executor = FlowExecutorView()
executor.plan = plan
executor.flow = flow
stage_view = StageView(executor)
self.assertEqual(ident, stage_view.get_context_data()["user"].username)

View File

@ -61,7 +61,7 @@ class DataclassEncoder(JSONEncoder):
return asdict(o)
if isinstance(o, UUID):
return str(o)
return super().default(o)
return super().default(o) # pragma: no cover
class EntryInvalidError(SentryIgnoredException):

View File

@ -11,7 +11,7 @@ from authentik.flows.transfer.common import (
FlowBundle,
FlowBundleEntry,
)
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
from authentik.policies.models import Policy, PolicyBinding
from authentik.stages.prompt.models import PromptStage
@ -31,11 +31,6 @@ class FlowExporter:
def _prepare_pbm(self):
self.pbm_uuids = [self.flow.pbm_uuid]
for stage_subclass in Stage.__subclasses__():
if issubclass(stage_subclass, PolicyBindingModel):
self.pbm_uuids += stage_subclass.objects.filter(
flow=self.flow
).values_list("pbm_uuid", flat=True)
self.pbm_uuids += FlowStageBinding.objects.filter(target=self.flow).values_list(
"pbm_uuid", flat=True
)

View File

@ -21,7 +21,12 @@ from authentik.audit.models import cleanse_dict
from authentik.core.models import USER_ATTRIBUTE_DEBUG
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan, FlowPlanner
from authentik.flows.planner import (
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_REDIRECT,
FlowPlan,
FlowPlanner,
)
from authentik.lib.utils.reflection import class_to_path
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
from authentik.policies.http import AccessDeniedResponse
@ -83,7 +88,9 @@ class FlowExecutorView(View):
return to_stage_response(self.request, self.handle_invalid_flow(exc))
except EmptyFlowException as exc:
LOGGER.warning("f(exec): Flow is empty", exc=exc)
return to_stage_response(self.request, self.handle_invalid_flow(exc))
# To match behaviour with loading an empty flow plan from cache,
# we don't show an error message here, but rather call _flow_done()
return self._flow_done()
# We don't save the Plan after getting the next stage
# as it hasn't been successfully passed yet
next_stage = self.plan.next(self.request)
@ -143,11 +150,15 @@ class FlowExecutorView(View):
"""User Successfully passed all stages"""
# Since this is wrapped by the ExecutorShell, the next argument is saved in the session
# extract the next param before cancel as that cleans it
next_param = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:shell"
)
next_param = None
if self.plan:
next_param = self.plan.context.get(PLAN_CONTEXT_REDIRECT)
if not next_param:
next_param = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:shell"
)
self.cancel()
return redirect_with_qs(next_param)
return to_stage_response(self.request, redirect_with_qs(next_param))
def stage_ok(self) -> HttpResponse:
"""Callback called by stages upon successful completion.

View File

@ -1,55 +0,0 @@
"""authentik lib navbar Templatetag"""
from django import template
from django.http import HttpRequest
from structlog import get_logger
register = template.Library()
LOGGER = get_logger()
ACTIVE_STRING = "pf-m-current"
@register.simple_tag(takes_context=True)
def is_active(context, *args: str, **_) -> str:
"""Return whether a navbar link is active or not."""
request: HttpRequest = context.get("request")
if not request.resolver_match:
return ""
match = request.resolver_match
for url in args:
if ":" in url:
app_name, url = url.split(":")
if match.app_name == app_name and match.url_name == url:
return ACTIVE_STRING
else:
if match.url_name == url:
return ACTIVE_STRING
return ""
@register.simple_tag(takes_context=True)
def is_active_url(context, view: str) -> str:
"""Return whether a navbar link is active or not."""
request: HttpRequest = context.get("request")
if not request.resolver_match:
return ""
match = request.resolver_match
current_full_url = f"{match.app_name}:{match.url_name}"
if current_full_url == view:
return ACTIVE_STRING
return ""
@register.simple_tag(takes_context=True)
def is_active_app(context, *args: str) -> str:
"""Return True if current link is from app"""
request: HttpRequest = context.get("request")
if not request.resolver_match:
return ""
for app_name in args:
if request.resolver_match.app_name == app_name:
return ACTIVE_STRING
return ""

View File

View File

@ -0,0 +1,18 @@
"""test sentry integration"""
from django.test import TestCase
from authentik.lib.sentry import SentryIgnoredException, before_send
class TestSentry(TestCase):
"""test sentry integration"""
def test_error_not_sent(self):
"""Test SentryIgnoredError not sent"""
self.assertIsNone(
before_send(None, {"exc_info": (0, SentryIgnoredException(), 0)})
)
def test_error_sent(self):
"""Test error sent"""
self.assertIsNone(before_send(None, {"exc_info": (0, ValueError(), 0)}))

View File

@ -20,6 +20,8 @@ class TestTimeUtils(TestCase):
"""Test invalid expression"""
with self.assertRaises(ValueError):
timedelta_from_string("foo")
with self.assertRaises(ValueError):
timedelta_from_string("bar=baz")
def test_validation(self):
"""Test Django model field validator"""

View File

@ -35,4 +35,6 @@ def timedelta_from_string(expr: str) -> datetime.timedelta:
if key.lower() not in ALLOWED_KEYS:
continue
kwargs[key.lower()] = float(value)
if len(kwargs) < 1:
raise ValueError("No valid keys to pass to timedelta")
return datetime.timedelta(**kwargs)

View File

@ -22,7 +22,6 @@ class AuthentikOutpostConfig(AppConfig):
name = "authentik.outposts"
label = "authentik_outposts"
mountpoint = "outposts/"
verbose_name = "authentik Outpost"
def ready(self):

View File

@ -2,8 +2,9 @@
from dataclasses import asdict, dataclass, field
from datetime import datetime
from enum import IntEnum
from typing import Any, Dict
from typing import Any, Dict, Optional
from channels.exceptions import DenyConnection
from dacite import from_dict
from dacite.data import Data
from guardian.shortcuts import get_objects_for_user
@ -39,18 +40,16 @@ class WebsocketMessage:
class OutpostConsumer(AuthJsonConsumer):
"""Handler for Outposts that connect over websockets for health checks and live updates"""
outpost: Outpost
outpost: Optional[Outpost] = None
def connect(self):
if not super().connect():
return
super().connect()
uuid = self.scope["url_route"]["kwargs"]["pk"]
outpost = get_objects_for_user(
self.user, "authentik_outposts.view_outpost"
).filter(pk=uuid)
if not outpost.exists():
self.close()
return
raise DenyConnection()
self.accept()
self.outpost = outpost.first()
OutpostState(
@ -60,7 +59,8 @@ class OutpostConsumer(AuthJsonConsumer):
# pylint: disable=unused-argument
def disconnect(self, close_code):
OutpostState.for_channel(self.outpost, self.channel_name).delete()
if self.outpost:
OutpostState.for_channel(self.outpost, self.channel_name).delete()
LOGGER.debug("removed channel from cache", channel_name=self.channel_name)
def receive_json(self, content: Data):

View File

@ -46,7 +46,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
"authentik_host_insecure": b64string(
str(self.controller.outpost.config.authentik_host_insecure)
),
"token": b64string(self.controller.outpost.token.token_uuid.hex),
"token": b64string(self.controller.outpost.token.key),
},
)

View File

@ -0,0 +1,38 @@
# Generated by Django 3.1.4 on 2020-12-13 14:07
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def update_config_prefix(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
alias = schema_editor.connection.alias
Outpost = apps.get_model("authentik_outposts", "Outpost")
for outpost in Outpost.objects.using(alias).all():
config = outpost._config
for key in list(config):
if "passbook" in key:
new_key = key.replace("passbook", "authentik")
config[new_key] = config[key]
del config[key]
outpost._config = config
outpost.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_outposts", "0013_auto_20201203_2009"),
]
operations = [
migrations.RunPython(update_config_prefix),
migrations.AlterField(
model_name="dockerserviceconnection",
name="url",
field=models.TextField(
help_text="Can be in the format of 'unix://<path>' when connecting to a local docker daemon, or 'https://<hostname>:2376' when connecting to a remote system."
),
),
]

View File

@ -113,17 +113,24 @@ class OutpostServiceConnection(models.Model):
objects = InheritanceManager()
@property
def state_key(self) -> str:
"""Key used to save connection state in cache"""
return f"outpost_service_connection_{self.pk.hex}"
@property
def state(self) -> OutpostServiceConnectionState:
"""Get state of service connection"""
state_key = f"outpost_service_connection_{self.pk.hex}"
state = cache.get(state_key, None)
from authentik.outposts.tasks import outpost_service_connection_state
state = cache.get(self.state_key, None)
if not state:
state = self._get_state()
cache.set(state_key, state, timeout=0)
outpost_service_connection_state.delay(self.pk)
return OutpostServiceConnectionState("", False)
return state
def _get_state(self) -> OutpostServiceConnectionState:
def fetch_state(self) -> OutpostServiceConnectionState:
"""Fetch current Service Connection state"""
raise NotImplementedError
@property
@ -140,7 +147,14 @@ class OutpostServiceConnection(models.Model):
class DockerServiceConnection(OutpostServiceConnection):
"""Service Connection to a Docker endpoint"""
url = models.TextField()
url = models.TextField(
help_text=_(
(
"Can be in the format of 'unix://<path>' when connecting to a local docker daemon, "
"or 'https://<hostname>:2376' when connecting to a remote system."
)
)
)
tls_verification = models.ForeignKey(
CertificateKeyPair,
null=True,
@ -196,7 +210,7 @@ class DockerServiceConnection(OutpostServiceConnection):
raise ServiceConnectionInvalid from exc
return client
def _get_state(self) -> OutpostServiceConnectionState:
def fetch_state(self) -> OutpostServiceConnectionState:
try:
client = self.client()
return OutpostServiceConnectionState(
@ -232,7 +246,7 @@ class KubernetesServiceConnection(OutpostServiceConnection):
def __str__(self) -> str:
return f"Kubernetes Service-Connection {self.name}"
def _get_state(self) -> OutpostServiceConnectionState:
def fetch_state(self) -> OutpostServiceConnectionState:
try:
client = self.client()
api_instance = VersionApi(client)
@ -240,7 +254,7 @@ class KubernetesServiceConnection(OutpostServiceConnection):
return OutpostServiceConnectionState(
version=version.git_version, healthy=True
)
except (OpenApiException, HTTPError):
except (OpenApiException, HTTPError, ServiceConnectionInvalid):
return OutpostServiceConnectionState(version="", healthy=False)
def client(self) -> ApiClient:

View File

@ -12,4 +12,9 @@ CELERY_BEAT_SCHEDULE = {
"schedule": crontab(minute=0, hour="*"),
"options": {"queue": "authentik_scheduled"},
},
"outpost_token_ensurer": {
"task": "authentik.outposts.tasks.outpost_token_ensurer",
"schedule": crontab(minute="*/5"),
"options": {"queue": "authentik_scheduled"},
},
}

View File

@ -35,21 +35,22 @@ def outpost_controller_all():
@CELERY_APP.task()
def outpost_service_connection_state(state_pk: Any):
def outpost_service_connection_state(connection_pk: Any):
"""Update cached state of a service connection"""
connection: OutpostServiceConnection = (
OutpostServiceConnection.objects.filter(pk=state_pk).select_subclasses().first()
OutpostServiceConnection.objects.filter(pk=connection_pk)
.select_subclasses()
.first()
)
cache.delete(f"outpost_service_connection_{connection.pk.hex}")
_ = connection.state
state = connection.fetch_state()
cache.set(connection.state_key, state, timeout=None)
@CELERY_APP.task(bind=True, base=MonitoredTask)
def outpost_service_connection_monitor(self: MonitoredTask):
"""Regularly check the state of Outpost Service Connections"""
for connection in OutpostServiceConnection.objects.select_subclasses():
cache.delete(f"outpost_service_connection_{connection.pk.hex}")
_ = connection.state
for connection in OutpostServiceConnection.objects.all():
outpost_service_connection_state.delay(connection.pk)
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
@ -90,6 +91,21 @@ def outpost_pre_delete(outpost_pk: str):
ProxyKubernetesController(outpost, service_connection).down()
@CELERY_APP.task(bind=True, base=MonitoredTask)
def outpost_token_ensurer(self: MonitoredTask):
"""Periodically ensure that all Outposts have valid Service Accounts
and Tokens"""
all_outposts = Outpost.objects.all()
for outpost in all_outposts:
_ = outpost.token
self.set_status(
TaskResult(
TaskResultStatus.SUCCESSFUL,
[f"Successfully checked {len(all_outposts)} Outposts."],
)
)
@CELERY_APP.task()
def outpost_post_save(model_class: str, model_pk: Any):
"""If an Outpost is saved, Ensure that token is created/updated

View File

@ -1,11 +0,0 @@
"""authentik outposts urls"""
from django.urls import path
from authentik.outposts.views import KubernetesManifestView, SetupView
urlpatterns = [
path(
"<uuid:outpost_pk>/k8s/", KubernetesManifestView.as_view(), name="k8s-manifest"
),
path("<uuid:outpost_pk>/", SetupView.as_view(), name="setup"),
]

View File

@ -1,89 +0,0 @@
"""authentik outpost views"""
from typing import Any, Dict, List
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Model
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from django.views import View
from django.views.generic import TemplateView
from guardian.shortcuts import get_objects_for_user
from structlog import get_logger
from authentik.core.models import User
from authentik.outposts.controllers.docker import DockerController
from authentik.outposts.models import (
DockerServiceConnection,
KubernetesServiceConnection,
Outpost,
OutpostType,
)
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
LOGGER = get_logger()
def get_object_for_user_or_404(user: User, perm: str, **filters) -> Model:
"""Wrapper that combines get_objects_for_user and get_object_or_404"""
return get_object_or_404(get_objects_for_user(user, perm), **filters)
class DockerComposeView(LoginRequiredMixin, View):
"""Generate docker-compose yaml"""
def get(self, request: HttpRequest, outpost_pk: str) -> HttpResponse:
"""Render docker-compose file"""
outpost: Outpost = get_object_for_user_or_404(
request.user,
"authentik_outposts.view_outpost",
pk=outpost_pk,
)
manifest = ""
if outpost.type == OutpostType.PROXY:
controller = DockerController(outpost, DockerServiceConnection())
manifest = controller.get_static_deployment()
return HttpResponse(manifest, content_type="text/vnd.yaml")
class KubernetesManifestView(LoginRequiredMixin, View):
"""Generate Kubernetes Deployment and SVC for proxy"""
def get(self, request: HttpRequest, outpost_pk: str) -> HttpResponse:
"""Render deployment template"""
outpost: Outpost = get_object_for_user_or_404(
request.user,
"authentik_outposts.view_outpost",
pk=outpost_pk,
)
manifest = ""
if outpost.type == OutpostType.PROXY:
controller = ProxyKubernetesController(
outpost, KubernetesServiceConnection()
)
manifest = controller.get_static_deployment()
return HttpResponse(manifest, content_type="text/vnd.yaml")
class SetupView(LoginRequiredMixin, TemplateView):
"""Setup view"""
def get_template_names(self) -> List[str]:
allowed = ["dc", "custom", "k8s_manual", "k8s_integration"]
setup_type = self.request.GET.get("type", "dc")
if setup_type not in allowed:
setup_type = allowed[0]
return [f"outposts/setup_{setup_type}.html"]
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
outpost: Outpost = get_object_for_user_or_404(
self.request.user,
"authentik_outposts.view_outpost",
pk=self.kwargs["outpost_pk"],
)
kwargs.update(
{"host": self.request.build_absolute_uri("/"), "outpost": outpost}
)
return kwargs

View File

@ -1,11 +1,16 @@
"""policy API Views"""
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.mixins import ListModelMixin
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import (
ModelSerializer,
PrimaryKeyRelatedField,
Serializer,
SerializerMethodField,
)
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.policies.forms import GENERAL_FIELDS
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
@ -68,6 +73,10 @@ class PolicyViewSet(ReadOnlyModelViewSet):
queryset = Policy.objects.all()
serializer_class = PolicySerializer
filterset_fields = {
"bindings": ["isnull"],
"promptstage": ["isnull"],
}
def get_queryset(self):
return Policy.objects.select_subclasses()
@ -98,3 +107,14 @@ class PolicyBindingViewSet(ModelViewSet):
serializer_class = PolicyBindingSerializer
filterset_fields = ["policy", "target", "enabled", "order", "timeout"]
search_fields = ["policy__name"]
class PolicyCacheViewSet(ListModelMixin, GenericViewSet):
"""Info about cached policies"""
queryset = Policy.objects.none()
serializer_class = Serializer
def list(self, request: Request) -> Response:
"""Info about cached policies"""
return Response(data={"pagination": {"count": len(cache.keys("policy_*"))}})

View File

@ -7,7 +7,7 @@
<label for="" class="pf-c-form__label"></label>
<div class="c-form__horizontal-group">
<p>
Expression using Python. See <a target="_blank" href="https://goauthentik.io/policies/expression/">here</a> for a list of all variables.
Expression using Python. See <a target="_blank" href="https://goauthentik.io/docs/policies/expression/">here</a> for a list of all variables.
</p>
</div>
</div>

View File

@ -50,6 +50,7 @@ class HaveIBeenPwendPolicy(Policy):
field=self.password_field,
fields=request.context.keys(),
)
return PolicyResult(False, _("Password not set in context"))
password = request.context[self.password_field]
pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec

View File

@ -10,6 +10,16 @@ from authentik.providers.oauth2.generators import generate_client_secret
class TestHIBPPolicy(TestCase):
"""Test HIBP Policy"""
def test_invalid(self):
"""Test without password"""
policy = HaveIBeenPwendPolicy.objects.create(
name="test_invalid",
)
request = PolicyRequest(get_anonymous_user())
result: PolicyResult = policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages[0], "Password not set in context")
def test_false(self):
"""Failing password case"""
policy = HaveIBeenPwendPolicy.objects.create(

View File

@ -50,6 +50,7 @@ class PasswordPolicy(Policy):
field=self.password_field,
fields=request.context.keys(),
)
return PolicyResult(False, _("Password not set in context"))
password = request.context[self.password_field]
filter_regex = []

View File

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

View File

@ -18,6 +18,7 @@ from django.utils import dateformat, timezone
from django.utils.translation import gettext_lazy as _
from jwkest.jwk import Key, RSAKey, SYMKey, import_rsa_key
from jwkest.jws import JWS
from rest_framework.serializers import Serializer
from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
from authentik.crypto.models import CertificateKeyPair
@ -263,6 +264,12 @@ class OAuth2Provider(Provider):
launch_url = urlparse(main_url)
return main_url.replace(launch_url.path, "")
@property
def serializer(self) -> Type[Serializer]:
from authentik.providers.oauth2.api import OAuth2ProviderSerializer
return OAuth2ProviderSerializer
@property
def form(self) -> Type[ModelForm]:
from authentik.providers.oauth2.forms import OAuth2ProviderForm

View File

@ -4,6 +4,16 @@
{% load i18n %}
{% load authentik_utils %}
{% block head %}
{{ block.super }}
<style>
.pf-c-background-image::before {
background-image: url("{% static 'dist/assets/images/flow_background.jpg' %}");
background-position: center;
}
</style>
{% endblock %}
{% block title %}
{% trans 'End session' %}
{% endblock %}

View File

@ -7,7 +7,7 @@
<label for="" class="pf-c-form__label"></label>
<div class="c-form__horizontal-group">
<p>
Expression using Python. See <a href="https://goauthentik.io/property-mappings/expression/">here</a> for a list of all variables.
Expression using Python. See <a href="https://goauthentik.io/docs/property-mappings/expression/">here</a> for a list of all variables.
</p>
</div>
</div>

View File

@ -0,0 +1,46 @@
"""Test authorize view"""
from django.test import RequestFactory, TestCase
from authentik.flows.models import Flow
from authentik.providers.oauth2.errors import (
AuthorizeError,
ClientIdError,
RedirectUriError,
)
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.providers.oauth2.views.authorize import OAuthAuthorizationParams
class TestViewsAuthorize(TestCase):
"""Test authorize view"""
def setUp(self) -> None:
super().setUp()
self.factory = RequestFactory()
def test_invalid_grant_type(self):
"""Test with invalid grant type"""
with self.assertRaises(AuthorizeError):
request = self.factory.get("/", data={"response_type": "invalid"})
OAuthAuthorizationParams.from_request(request)
def test_invalid_client_id(self):
"""Test invalid client ID"""
with self.assertRaises(ClientIdError):
request = self.factory.get(
"/", data={"response_type": "code", "client_id": "invalid"}
)
OAuthAuthorizationParams.from_request(request)
def test_missing_redirect_uri(self):
"""test missing redirect URI"""
OAuth2Provider.objects.create(
name="test",
client_id="test",
authorization_flow=Flow.objects.first(),
)
with self.assertRaises(RedirectUriError):
request = self.factory.get(
"/", data={"response_type": "code", "client_id": "test"}
)
OAuthAuthorizationParams.from_request(request)

View File

@ -139,7 +139,7 @@ class OAuthAuthorizationParams:
is_open_id = SCOPE_OPENID in self.scope
# Redirect URI validation.
if is_open_id and not self.redirect_uri:
if not self.redirect_uri:
LOGGER.warning("Missing redirect uri.")
raise RedirectUriError()
if self.redirect_uri.lower() not in [

View File

@ -1,34 +1,6 @@
# Generated by Django 3.1.1 on 2020-09-30 08:10
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
SCOPE_AK_PROXY_EXPRESSION = """return {
"ak_proxy": {
"user_attributes": user.group_attributes()
}
}"""
def create_proxy_scope(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.providers.proxy.models import SCOPE_AK_PROXY, ProxyProvider
ScopeMapping = apps.get_model("authentik_providers_oauth2", "ScopeMapping")
ScopeMapping.objects.update_or_create(
scope_name=SCOPE_AK_PROXY,
defaults={
"name": "Autogenerated OAuth2 Mapping: authentik Proxy",
"scope_name": SCOPE_AK_PROXY,
"description": "",
"expression": SCOPE_AK_PROXY_EXPRESSION,
},
)
for provider in ProxyProvider.objects.all():
provider.set_oauth_defaults()
provider.save()
class Migration(migrations.Migration):
@ -74,5 +46,4 @@ class Migration(migrations.Migration):
verbose_name="HTTP-Basic Username",
),
),
migrations.RunPython(create_proxy_scope),
]

View File

@ -0,0 +1,41 @@
# Generated by Django 3.1.4 on 2020-12-14 09:42
from django.apps.registry import Apps
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
SCOPE_AK_PROXY_EXPRESSION = """return {
"ak_proxy": {
"user_attributes": user.group_attributes()
}
}"""
def create_proxy_scope(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.providers.proxy.models import SCOPE_AK_PROXY, ProxyProvider
ScopeMapping = apps.get_model("authentik_providers_oauth2", "ScopeMapping")
ScopeMapping.objects.filter(scope_name="pb_proxy").delete()
ScopeMapping.objects.update_or_create(
scope_name=SCOPE_AK_PROXY,
defaults={
"name": "Autogenerated OAuth2 Mapping: authentik Proxy",
"scope_name": SCOPE_AK_PROXY,
"description": "",
"expression": SCOPE_AK_PROXY_EXPRESSION,
},
)
for provider in ProxyProvider.objects.all():
provider.set_oauth_defaults()
provider.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_proxy", "0009_auto_20201007_1721"),
]
operations = [migrations.RunPython(create_proxy_scope)]

View File

@ -8,6 +8,7 @@ from django.db import models
from django.forms import ModelForm
from django.http import HttpRequest
from django.utils.translation import gettext as _
from rest_framework.serializers import Serializer
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.models import DomainlessURLValidator
@ -108,6 +109,12 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
return ProxyProviderForm
@property
def serializer(self) -> Type[Serializer]:
from authentik.providers.proxy.api import ProxyProviderSerializer
return ProxyProviderSerializer
@property
def launch_url(self) -> Optional[str]:
"""Use external_host as launch URL"""

View File

@ -7,6 +7,7 @@ from django.forms import ModelForm
from django.http import HttpRequest
from django.shortcuts import reverse
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from structlog import get_logger
from authentik.core.models import PropertyMapping, Provider
@ -145,6 +146,12 @@ class SAMLProvider(Provider):
launch_url = urlparse(self.acs_url)
return self.acs_url.replace(launch_url.path, "")
@property
def serializer(self) -> Type[Serializer]:
from authentik.providers.saml.api import SAMLPropertyMappingSerializer
return SAMLPropertyMappingSerializer
@property
def form(self) -> Type[ModelForm]:
from authentik.providers.saml.forms import SAMLProviderForm

View File

@ -7,7 +7,7 @@
<label for="" class="pf-c-form__label"></label>
<div class="c-form__horizontal-group">
<p>
Expression using Python. See <a href="https://goauthentik.io/property-mappings/expression/">here</a> for a list of all variables.
Expression using Python. See <a href="https://goauthentik.io/docs/property-mappings/expression/">here</a> for a list of all variables.
</p>
</div>
</div>

View File

@ -0,0 +1,84 @@
"""Test Requests and Responses against schema"""
from base64 import b64encode
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import RequestFactory, TestCase
from guardian.utils import get_anonymous_user
from lxml import etree # nosec
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.assertion import AssertionProcessor
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
from authentik.providers.saml.tests.test_auth_n_request import dummy_get_response
from authentik.sources.saml.models import SAMLSource
from authentik.sources.saml.processors.request import RequestProcessor
class TestSchema(TestCase):
"""Test Requests and Responses against schema"""
def setUp(self):
cert = CertificateKeyPair.objects.first()
self.provider: SAMLProvider = SAMLProvider.objects.create(
authorization_flow=Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
),
acs_url="http://testserver/source/saml/provider/acs/",
signing_kp=cert,
verification_kp=cert,
)
self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
self.provider.save()
self.source = SAMLSource.objects.create(
slug="provider",
issuer="authentik",
signing_kp=cert,
)
self.factory = RequestFactory()
def test_request_schema(self):
"""Test generated AuthNRequest against Schema"""
http_request = self.factory.get("/")
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(http_request)
http_request.session.save()
# First create an AuthNRequest
request_proc = RequestProcessor(self.source, http_request, "test_state")
request = request_proc.build_auth_n()
metadata = etree.fromstring(request) # nosec
schema = etree.XMLSchema(
etree.parse("xml/saml-schema-protocol-2.0.xsd")
) # nosec
self.assertTrue(schema.validate(metadata))
def test_response_schema(self):
"""Test generated AuthNRequest against Schema"""
http_request = self.factory.get("/")
http_request.user = get_anonymous_user()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(http_request)
http_request.session.save()
# First create an AuthNRequest
request_proc = RequestProcessor(self.source, http_request, "test_state")
request = request_proc.build_auth_n()
# To get an assertion we need a parsed request (parsed by provider)
parsed_request = AuthNRequestParser(self.provider).parse(
b64encode(request.encode()).decode(), "test_state"
)
# Now create a response and convert it to string (provider)
response_proc = AssertionProcessor(self.provider, http_request, parsed_request)
response = response_proc.build_response()
metadata = etree.fromstring(response) # nosec
schema = etree.XMLSchema(etree.parse("xml/saml-schema-protocol-2.0.xsd"))
self.assertTrue(schema.validate(metadata))

View File

@ -1,15 +1,6 @@
"""Small helper functions"""
import uuid
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.template.context import Context
def render_xml(request: HttpRequest, template: str, ctx: Context) -> HttpResponse:
"""Render template with content_type application/xml"""
return render(request, template, context=ctx, content_type="application/xml")
def get_random_id() -> str:
"""Random hex id"""

View File

@ -32,3 +32,10 @@ class TestRecovery(TestCase):
reverse("authentik_recovery:use-token", kwargs={"key": token.key})
)
self.assertEqual(int(self.client.session["_auth_user_id"]), token.user.pk)
def test_recovery_view_invalid(self):
"""Test recovery view with invalid token"""
response = self.client.get(
reverse("authentik_recovery:use-token", kwargs={"key": "abc"})
)
self.assertEqual(response.status_code, 404)

View File

@ -7,13 +7,16 @@ class MessageConsumer(JsonWebsocketConsumer):
"""Consumer which sends django.contrib.messages Messages over WS.
channel_name is saved into cache with user_id, and when a add_message is called"""
session_key: str
def connect(self):
self.accept()
cache.set(f"user_{self.scope['user'].pk}_messages_{self.channel_name}", True)
self.session_key = self.scope["session"].session_key
cache.set(f"user_{self.session_key}_messages_{self.channel_name}", True, None)
# pylint: disable=unused-argument
def disconnect(self, close_code):
cache.delete(f"user_{self.scope['user'].pk}_messages_{self.channel_name}")
cache.delete(f"user_{self.session_key}_messages_{self.channel_name}")
def event_update(self, event: dict):
"""Event handler which is called by Messages Storage backend"""

View File

@ -16,7 +16,7 @@ class ChannelsStorage(FallbackStorage):
self.channel = get_channel_layer()
def _store(self, messages: list[Message], response, *args, **kwargs):
prefix = f"user_{self.request.user.pk}_messages_"
prefix = f"user_{self.request.session.session_key}_messages_"
keys = cache.keys(f"{prefix}*")
if len(keys) < 1:
return super()._store(messages, response, *args, **kwargs)

View File

@ -4,7 +4,7 @@ from django.conf import settings
from authentik.lib.config import CONFIG
class PytestTestRunner:
class PytestTestRunner: # pragma: no cover
"""Runs pytest to discover and run tests."""
def __init__(self, verbosity=1, failfast=False, keepdb=False, **_):

View File

@ -7,7 +7,7 @@
<label for="" class="pf-c-form__label"></label>
<div class="c-form__horizontal-group">
<p>
Expression using Python. See <a href="https://goauthentik.io/property-mappings/expression/">here</a> for a list of all variables.
Expression using Python. See <a href="https://goauthentik.io/docs/property-mappings/expression/">here</a> for a list of all variables.
</p>
</div>
</div>

View File

@ -0,0 +1,41 @@
"""Discord Type tests"""
from django.test import TestCase
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.discord import DiscordOAuth2Callback
# https://discord.com/developers/docs/resources/user#user-object
DISCORD_USER = {
"id": "80351110224678912",
"username": "Nelly",
"discriminator": "1337",
"avatar": "8342729096ea3675442027381ff50dfe",
"verified": True,
"email": "nelly@discord.com",
"flags": 64,
"premium_type": 1,
"public_flags": 64,
}
class TestTypeGitHub(TestCase):
"""OAuth Source tests"""
def setUp(self):
self.source = OAuthSource.objects.create(
name="test",
slug="test",
provider_type="openid-connect",
authorization_url="",
profile_url="",
consumer_key="",
)
def test_enroll_context(self):
"""Test GitHub Enrollment context"""
ak_context = DiscordOAuth2Callback().get_user_enroll_context(
self.source, UserOAuthSourceConnection(), DISCORD_USER
)
self.assertEqual(ak_context["username"], DISCORD_USER["username"])
self.assertEqual(ak_context["email"], DISCORD_USER["email"])
self.assertEqual(ak_context["name"], DISCORD_USER["username"])

View File

@ -0,0 +1,71 @@
"""GitHub Type tests"""
from django.test import TestCase
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.github import GitHubOAuth2Callback
# https://developer.github.com/v3/users/#get-the-authenticated-user
GITHUB_USER = {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": False,
"name": "monalisa octocat",
"company": "GitHub",
"blog": "https://github.com/blog",
"location": "San Francisco",
"email": "octocat@github.com",
"hireable": False,
"bio": "There once was...",
"twitter_username": "monatheoctocat",
"public_repos": 2,
"public_gists": 1,
"followers": 20,
"following": 0,
"created_at": "2008-01-14T04:33:35Z",
"updated_at": "2008-01-14T04:33:35Z",
"private_gists": 81,
"total_private_repos": 100,
"owned_private_repos": 100,
"disk_usage": 10000,
"collaborators": 8,
"two_factor_authentication": True,
"plan": {"name": "Medium", "space": 400, "private_repos": 20, "collaborators": 0},
}
class TestTypeGitHub(TestCase):
"""OAuth Source tests"""
def setUp(self):
self.source = OAuthSource.objects.create(
name="test",
slug="test",
provider_type="openid-connect",
authorization_url="",
profile_url="",
consumer_key="",
)
def test_enroll_context(self):
"""Test GitHub Enrollment context"""
ak_context = GitHubOAuth2Callback().get_user_enroll_context(
self.source, UserOAuthSourceConnection(), GITHUB_USER
)
self.assertEqual(ak_context["username"], GITHUB_USER["login"])
self.assertEqual(ak_context["email"], GITHUB_USER["email"])
self.assertEqual(ak_context["name"], GITHUB_USER["name"])

View File

@ -0,0 +1,112 @@
"""Twitter Type tests"""
from django.test import Client, TestCase
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.twitter import TwitterOAuthCallback
# https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/ \
# api-reference/get-account-verify_credentials
TWITTER_USER = {
"contributors_enabled": True,
"created_at": "Sat May 09 17:58:22 +0000 2009",
"default_profile": False,
"default_profile_image": False,
"description": "I taught your phone that thing you like.",
"favourites_count": 588,
"follow_request_sent": None,
"followers_count": 10625,
"following": None,
"friends_count": 1181,
"geo_enabled": True,
"id": 38895958,
"id_str": "38895958",
"is_translator": False,
"lang": "en",
"listed_count": 190,
"location": "San Francisco",
"name": "Sean Cook",
"notifications": None,
"profile_background_color": "1A1B1F",
"profile_background_image_url": "",
"profile_background_image_url_https": "",
"profile_background_tile": True,
"profile_image_url": "",
"profile_image_url_https": "",
"profile_link_color": "2FC2EF",
"profile_sidebar_border_color": "181A1E",
"profile_sidebar_fill_color": "252429",
"profile_text_color": "666666",
"profile_use_background_image": True,
"protected": False,
"screen_name": "theSeanCook",
"show_all_inline_media": True,
"status": {
"contributors": None,
"coordinates": {"coordinates": [-122.45037293, 37.76484123], "type": "Point"},
"created_at": "Tue Aug 28 05:44:24 +0000 2012",
"favorited": False,
"geo": {"coordinates": [37.76484123, -122.45037293], "type": "Point"},
"id": 240323931419062272,
"id_str": "240323931419062272",
"in_reply_to_screen_name": "messl",
"in_reply_to_status_id": 240316959173009410,
"in_reply_to_status_id_str": "240316959173009410",
"in_reply_to_user_id": 18707866,
"in_reply_to_user_id_str": "18707866",
"place": {
"attributes": {},
"bounding_box": {
"coordinates": [
[
[-122.45778216, 37.75932999],
[-122.44248216, 37.75932999],
[-122.44248216, 37.76752899],
[-122.45778216, 37.76752899],
]
],
"type": "Polygon",
},
"country": "United States",
"country_code": "US",
"full_name": "Ashbury Heights, San Francisco",
"id": "866269c983527d5a",
"name": "Ashbury Heights",
"place_type": "neighborhood",
"url": "http://api.twitter.com/1/geo/id/866269c983527d5a.json",
},
"retweet_count": 0,
"retweeted": False,
"source": "Twitter for iPhone",
"text": "@messl congrats! So happy for all 3 of you.",
"truncated": False,
},
"statuses_count": 2609,
"time_zone": "Pacific Time (US & Canada)",
"url": None,
"utc_offset": -28800,
"verified": False,
}
class TestTypeGitHub(TestCase):
"""OAuth Source tests"""
def setUp(self):
self.client = Client()
self.source = OAuthSource.objects.create(
name="test",
slug="test",
provider_type="openid-connect",
authorization_url="",
profile_url="",
consumer_key="",
)
def test_enroll_context(self):
"""Test Twitter Enrollment context"""
ak_context = TwitterOAuthCallback().get_user_enroll_context(
self.source, UserOAuthSourceConnection(), TWITTER_USER
)
self.assertEqual(ak_context["username"], TWITTER_USER["screen_name"])
self.assertEqual(ak_context["email"], TWITTER_USER.get("email", None))
self.assertEqual(ak_context["name"], TWITTER_USER["name"])

View File

@ -1,15 +1,14 @@
"""OAuth Source tests"""
from django.shortcuts import reverse
from django.test import Client, TestCase
from django.test import TestCase
from authentik.sources.oauth.models import OAuthSource
class OAuthSourceTests(TestCase):
class TestOAuthSource(TestCase):
"""OAuth Source tests"""
def setUp(self):
self.client = Client()
self.source = OAuthSource.objects.create(
name="test",
slug="test",

View File

@ -11,7 +11,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
class DiscordOAuthRedirect(OAuthRedirect):
"""Discord OAuth2 Redirect"""
def get_additional_parameters(self, source):
def get_additional_parameters(self, source): # pragma: no cover
return {
"scope": "email identify",
}

View File

@ -14,7 +14,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
class FacebookOAuthRedirect(OAuthRedirect):
"""Facebook OAuth2 Redirect"""
def get_additional_parameters(self, source):
def get_additional_parameters(self, source): # pragma: no cover
return {
"scope": "email",
}

View File

@ -11,7 +11,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
class GoogleOAuthRedirect(OAuthRedirect):
"""Google OAuth2 Redirect"""
def get_additional_parameters(self, source):
def get_additional_parameters(self, source): # pragma: no cover
return {
"scope": "email profile",
}

View File

@ -11,7 +11,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
class OpenIDConnectOAuthRedirect(OAuthRedirect):
"""OpenIDConnect OAuth2 Redirect"""
def get_additional_parameters(self, source: OAuthSource):
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
return {
"scope": "openid email profile",
}

View File

@ -14,7 +14,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
class RedditOAuthRedirect(OAuthRedirect):
"""Reddit OAuth2 Redirect"""
def get_additional_parameters(self, source):
def get_additional_parameters(self, source): # pragma: no cover
return {
"scope": "identity",
"duration": "permanent",

View File

@ -18,6 +18,6 @@ class TwitterOAuthCallback(OAuthCallback):
) -> Dict[str, Any]:
return {
"username": info.get("screen_name"),
"email": info.get("email"),
"email": info.get("email", None),
"name": info.get("name"),
}

View File

@ -15,10 +15,11 @@ from authentik.core.models import User
from authentik.flows.models import Flow, in_memory_stage
from authentik.flows.planner import (
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_REDIRECT,
PLAN_CONTEXT_SSO,
FlowPlanner,
)
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.utils import delete_none_keys
from authentik.sources.oauth.auth import AuthorizedServiceBackend
@ -135,11 +136,17 @@ class OAuthCallback(OAuthClientMixin, View):
def handle_login_flow(self, flow: Flow, **kwargs) -> HttpResponse:
"""Prepare Authentication Plan, redirect user FlowExecutor"""
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:shell"
)
kwargs.update(
{
# Since we authenticate the user by their token, they have no backend set
PLAN_CONTEXT_AUTHENTICATION_BACKEND: "django.contrib.auth.backends.ModelBackend",
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_REDIRECT: final_redirect,
}
)
# We run the Flow planner here so we can pass the Pending user in the context

View File

@ -13,10 +13,11 @@ from authentik.core.models import User
from authentik.flows.models import Flow
from authentik.flows.planner import (
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_REDIRECT,
PLAN_CONTEXT_SSO,
FlowPlanner,
)
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.utils import delete_none_keys
from authentik.sources.saml.exceptions import (
@ -54,11 +55,14 @@ class ResponseProcessor:
_root: Any
_root_xml: str
_http_request: HttpRequest
def __init__(self, source: SAMLSource):
self._source = source
def parse(self, request: HttpRequest):
"""Check if `request` contains SAML Response data, parse and validate it."""
self._http_request = request
# First off, check if we have any SAML Data at all.
raw_response = request.POST.get("SAMLResponse", None)
if not raw_response:
@ -187,6 +191,11 @@ class ResponseProcessor:
name_id_filter = self._get_name_id_filter()
matching_users = User.objects.filter(**name_id_filter)
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self._http_request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:shell"
)
if matching_users.exists():
# User exists already, switch to authentication flow
return self._flow_response(
@ -195,6 +204,7 @@ class ResponseProcessor:
**{
PLAN_CONTEXT_PENDING_USER: matching_users.first(),
PLAN_CONTEXT_AUTHENTICATION_BACKEND: DEFAULT_BACKEND,
PLAN_CONTEXT_REDIRECT: final_redirect,
},
)
return self._flow_response(

View File

@ -1,26 +0,0 @@
"""SAML Source tests"""
from defusedxml import ElementTree
from django.test import RequestFactory, TestCase
from authentik.crypto.models import CertificateKeyPair
from authentik.sources.saml.models import SAMLSource
from authentik.sources.saml.processors.metadata import MetadataProcessor
class TestMetadataProcessor(TestCase):
"""Test MetadataProcessor"""
def setUp(self):
self.source = SAMLSource.objects.create(
slug="provider",
issuer="authentik",
signing_kp=CertificateKeyPair.objects.first(),
)
self.factory = RequestFactory()
def test_metadata(self):
"""Test Metadata generation being valid"""
request = self.factory.get("/")
xml = MetadataProcessor(self.source, request).build_entity_descriptor()
metadata = ElementTree.fromstring(xml)
self.assertEqual(metadata.attrib["entityID"], "authentik")

View File

View File

@ -0,0 +1,55 @@
"""SAML Source tests"""
from defusedxml import ElementTree
from django.test import RequestFactory, TestCase
from lxml import etree # nosec
from authentik.crypto.models import CertificateKeyPair
from authentik.sources.saml.models import SAMLSource
from authentik.sources.saml.processors.metadata import MetadataProcessor
class TestMetadataProcessor(TestCase):
"""Test MetadataProcessor"""
def setUp(self):
self.factory = RequestFactory()
def test_metadata_schema(self):
"""Test Metadata generation being valid"""
source = SAMLSource.objects.create(
slug="provider",
issuer="authentik",
signing_kp=CertificateKeyPair.objects.first(),
)
request = self.factory.get("/")
xml = MetadataProcessor(source, request).build_entity_descriptor()
metadata = etree.fromstring(xml) # nosec
schema = etree.XMLSchema(
etree.parse("xml/saml-schema-metadata-2.0.xsd")
) # nosec
self.assertTrue(schema.validate(metadata))
def test_metadata(self):
"""Test Metadata generation being valid"""
source = SAMLSource.objects.create(
slug="provider",
issuer="authentik",
signing_kp=CertificateKeyPair.objects.first(),
)
request = self.factory.get("/")
xml = MetadataProcessor(source, request).build_entity_descriptor()
metadata = ElementTree.fromstring(xml)
self.assertEqual(metadata.attrib["entityID"], "authentik")
def test_metadata_without_signautre(self):
"""Test Metadata generation being valid"""
source = SAMLSource.objects.create(
slug="provider",
issuer="authentik",
# signing_kp=CertificateKeyPair.objects.first(),
)
request = self.factory.get("/")
xml = MetadataProcessor(source, request).build_entity_descriptor()
metadata = ElementTree.fromstring(xml)
self.assertEqual(metadata.attrib["entityID"], "authentik")

View File

@ -22,10 +22,10 @@
</ul>
{% if not state %}
{% if stage.configure_flow %}
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={{ request.get_full_path }}" class="pf-c-button pf-m-primary">{% trans "Enable Static Tokens" %}</a>
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={% url 'authentik_core:user-settings' %}" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Static Tokens" %}</a>
{% endif %}
{% else %}
<a href="{% url 'authentik_stages_otp_static:disable' stage_uuid=stage.stage_uuid %}" class="pf-c-button pf-m-danger">{% trans "Disable Static Tokens" %}</a>
<a href="{% url 'authentik_stages_otp_static:disable' stage_uuid=stage.stage_uuid %}" class="ak-root-pf-c-button pf-m-danger">{% trans "Disable Static Tokens" %}</a>
{% endif %}
</div>
</div>

View File

@ -41,4 +41,4 @@ class DisableView(LoginRequiredMixin, View):
Event.new(
"static_otp_disable", message="User disabled Static OTP Tokens."
).from_http(request)
return redirect("authentik_stages_otp:otp-user-settings")
return redirect("authentik_core:user-settings")

View File

@ -18,10 +18,10 @@
<p>
{% if not state %}
{% if stage.configure_flow %}
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={{ request.get_full_path }}" class="pf-c-button pf-m-primary">{% trans "Enable Time-based OTP" %}</a>
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={% url 'authentik_core:user-settings' %}" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Time-based OTP" %}</a>
{% endif %}
{% else %}
<a href="{% url 'authentik_stages_otp_time:disable' stage_uuid=stage.stage_uuid %}" class="pf-c-button pf-m-danger">{% trans "Disable Time-based OTP" %}</a>
<a href="{% url 'authentik_stages_otp_time:disable' stage_uuid=stage.stage_uuid %}" class="ak-root-pf-c-button pf-m-danger">{% trans "Disable Time-based OTP" %}</a>
{% endif %}
</p>
</div>

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