Compare commits

...

431 Commits

Author SHA1 Message Date
e13d348315 new release: 0.12.2-stable 2020-10-19 19:36:36 +02:00
169f3ebe5b outposts: fix logger again 2020-10-19 18:52:17 +02:00
f8ad604e85 outposts: add more tests 2020-10-19 17:47:51 +02:00
774b9c8a61 outposts: update kubernetes controller to use pk as identifier instead of name 2020-10-19 17:39:12 +02:00
d8c522233e outposts: fix outpost mangling log output 2020-10-19 16:54:11 +02:00
82d50f7eaa outposts: fix list showing questionmark when only one outpost is registered 2020-10-19 16:34:16 +02:00
1c426c5136 outposts: trigger deployment re-create when selector changes 2020-10-19 16:21:39 +02:00
d6e14cc551 proxy: show version on startup 2020-10-19 16:21:13 +02:00
c3917ebc2e lifecycle: fix formatting 2020-10-19 16:13:45 +02:00
7203bd37a3 outposts: replace migration with string backup handler 2020-10-19 16:04:38 +02:00
597188c7ee lifecycle: fix migration trying to load all classes 2020-10-19 15:55:16 +02:00
ac4c314042 new release: 0.12.1-stable 2020-10-19 15:30:27 +02:00
05866d3544 providers/proxy: fix creation of ingress 2020-10-19 15:06:50 +02:00
6596bc6034 helm: fix permissions for ingresses in networking 2020-10-19 14:55:14 +02:00
c6661ef4d2 lifecycle: add migration to 0.12 which removes old outpost state from cache 2020-10-19 14:35:38 +02:00
386e23dfac core: fix api signature for view_key 2020-10-19 14:35:22 +02:00
5d7220ca70 helm: fix keys for s3 backup 2020-10-19 14:30:44 +02:00
5de0d03acf new release: 0.12.0-stable 2020-10-19 12:15:25 +02:00
b0cc91f343 ci: disable code-ql while django check is broken 2020-10-19 12:15:17 +02:00
029a78f108 build(deps): bump sentry-sdk from 0.19.0 to 0.19.1 (#285)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 0.19.0 to 0.19.1.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGES.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/0.19.0...0.19.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-19 12:02:33 +02:00
3f4a8dc4f6 docs: update example helm values file 2020-10-19 11:31:36 +02:00
32f6ba6302 ci: install python3.8 for code ql 2020-10-19 11:12:57 +02:00
8da0b14f29 docs: update to-012 2020-10-19 11:12:57 +02:00
83eb4aff02 build(deps): bump rollup in /passbook/static/static (#282)
Bumps [rollup](https://github.com/rollup/rollup) from 2.31.0 to 2.32.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.31.0...v2.32.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-19 11:12:46 +02:00
927d02f591 build(deps): bump celery from 5.0.0 to 5.0.1 (#279)
Bumps [celery](https://github.com/celery/celery) from 5.0.0 to 5.0.1.
- [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.0...v5.0.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-19 11:12:34 +02:00
d04afcd6d0 build(deps): bump chart.js in /passbook/static/static (#283)
Bumps [chart.js](https://github.com/chartjs/Chart.js) from 2.9.3 to 2.9.4.
- [Release notes](https://github.com/chartjs/Chart.js/releases)
- [Commits](https://github.com/chartjs/Chart.js/compare/v2.9.3...v2.9.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-19 10:44:05 +02:00
89c6db66fd build(deps): bump drf-yasg2 from 1.18.5 to 1.19.2 (#284)
Bumps [drf-yasg2](https://github.com/JoelLefkowitz/drf-yasg) from 1.18.5 to 1.19.2.
- [Release notes](https://github.com/JoelLefkowitz/drf-yasg/releases)
- [Changelog](https://github.com/JoelLefkowitz/drf-yasg/blob/master/docs/changelog.rst)
- [Commits](https://github.com/JoelLefkowitz/drf-yasg/compare/1.18.5...1.19.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-19 10:43:52 +02:00
e6ffa65a7e build(deps): bump lxml from 4.5.2 to 4.6.1 (#280)
Bumps [lxml](https://github.com/lxml/lxml) from 4.5.2 to 4.6.1.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.5.2...lxml-4.6.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-19 08:38:58 +02:00
8a2f982a77 admin: fix html of overview 2020-10-19 00:46:31 +02:00
16cf6315e3 docs: add active directory docs 2020-10-18 23:03:46 +02:00
1d85874f41 stages/user_write: don't update session hash after password change when impersonating 2020-10-18 22:58:05 +02:00
ff64182ae8 stages/prompt: skip password equality check when previous checks failed 2020-10-18 22:54:40 +02:00
a9ee67bf2d sources/ldap: adjust order or fields in form 2020-10-18 22:39:13 +02:00
e87d52a76b providers/proxy: implement Ingress diff checking 2020-10-18 21:34:45 +02:00
8b09cf55a2 root: upgrade to traefik 2.3 2020-10-18 18:48:19 +02:00
0203d20759 providers/proxy: add traefik labels to outposts deployed with docker integration 2020-10-18 17:46:20 +02:00
7861e2e0bd provider/proxy: add K8s ingress support 2020-10-18 17:13:44 +02:00
ad29d54bbf outposts: simplify k8s controller add more extensibility 2020-10-18 17:09:02 +02:00
c698ba37d9 core: add ability for users to create tokens 2020-10-18 15:42:16 +02:00
6a53069653 *: make generic template's base parameterised 2020-10-18 15:35:27 +02:00
152b2d863d api: add fallback for proxies < 0.12 which send authorization without b64 2020-10-18 15:14:00 +02:00
ee670d5e19 core: add key field to token for easier rotation 2020-10-18 14:34:22 +02:00
36e095671c proxy: fix WS Authorization Header being sent with the wrong format 2020-10-18 14:04:12 +02:00
1088b947a8 audit: remove duplicate date column, add search 2020-10-17 22:26:35 +02:00
c4a30c50ac stages/consent: add fallback template 2020-10-17 18:18:29 +02:00
2831df45a0 docs: add note about high cpu usage of proxy 2020-10-17 17:06:57 +02:00
ee5bac099f outposts: fix migration not having access to token property 2020-10-17 17:06:08 +02:00
69f7b41044 e2e: use dockercontroller to test proxy 2020-10-17 17:03:10 +02:00
f9cede7b31 proxy: add random reload offset for HA 2020-10-17 16:48:53 +02:00
903cdeaa7f proxy: fix high CPU when websocket not connected 2020-10-17 16:44:53 +02:00
e909e7fa8a outposts: kill container on down 2020-10-17 16:33:38 +02:00
bee38551f3 outposts: fix tokens without identifier not loading in the UI 2020-10-17 16:33:23 +02:00
c0ec6388df outposts: give container time to boot when newly created 2020-10-16 23:38:46 +02:00
8f08836885 outposts: ensure log is also written to stdout 2020-10-16 23:36:59 +02:00
dd0d7e7481 root: switch from drf-yasg to drf_yasg2 and up rest_framework 2020-10-16 23:32:35 +02:00
25d0ac6534 ci: bump pyright version 2020-10-16 22:29:59 +02:00
971713d1aa outposts: call controller.down on outpost pre_delete 2020-10-16 22:27:00 +02:00
5135d828b4 outposts: rename run to up, add down method for deleting 2020-10-16 22:22:15 +02:00
b2c571bf1b helm: add service account for controller, add option to enable it 2020-10-16 21:55:24 +02:00
6b1d30d230 outposts: improve logging from k8s controller 2020-10-16 21:31:55 +02:00
3454760731 *: ensure TaskResult uid is slugified to prevent URL errors 2020-10-16 21:31:12 +02:00
96846220c3 outposts: trigger reconcile on save 2020-10-16 21:08:35 +02:00
a4f5678144 docs: update admin screenshot 2020-10-16 20:26:09 +02:00
a18baa3cb3 static: simplify Message update trigger 2020-10-16 20:13:57 +02:00
dfedd4a7f1 admin: improve overview, re-add links
closes #270
2020-10-16 20:07:56 +02:00
897f64600a static: dynamically add messages instead of replacing 2020-10-16 19:22:44 +02:00
c6eb015d18 static: fix shell card missing on small screens 2020-10-16 19:13:39 +02:00
54088239ab sources/ldap: fix MonitoredTask not using uid 2020-10-16 16:43:40 +02:00
aa9c7a6567 flow: re-add FlowShell as Web Component 2020-10-16 16:36:18 +02:00
6c0c12c90a static: fix messages update only working once 2020-10-16 16:30:38 +02:00
c49b57ad1d stages/email: fix make_msgid call 2020-10-16 16:07:59 +02:00
2339e855bb *: Improve MonitoredTasks' error capture 2020-10-16 16:00:24 +02:00
bdc019c7cf outposts: skip post_save during migrations and unittests 2020-10-16 15:58:28 +02:00
5e2fb6d56e static: replace server-side alerts with webcomponent 2020-10-16 15:26:51 +02:00
3b9524cdfc *: ensure unittests wait on tasks 2020-10-16 14:53:14 +02:00
7154f19668 admin: fix task list not being sorted 2020-10-16 14:53:00 +02:00
8fedd9ec07 stages/email: Implement MonitoredTask, but only for failed emails 2020-10-16 14:31:01 +02:00
4ac87d8739 sources/saml: Implement MonitoredTask 2020-10-16 14:30:44 +02:00
e4f45eba0a policies/reputation: implement MonitoredTask 2020-10-16 14:20:41 +02:00
4b3e0f0f96 sources/ldap: implement MonitoredTask 2020-10-16 14:20:07 +02:00
482da81522 admin: add button to retry task 2020-10-16 14:10:27 +02:00
c5226fd0e8 admin: add API to list tasks and schedule retry 2020-10-16 14:10:11 +02:00
7806cff96f lib: save task's call arguments for manual retry 2020-10-16 13:35:40 +02:00
fa504e4bf9 outposts: pass outpost reference instead of PK, implement TaskResult.uid 2020-10-16 12:54:52 +02:00
86cfb10b9b outposts: implement .run_wuth_logs() which returns logs, add task monitoring 2020-10-16 11:38:49 +02:00
f6b8171624 outposts: improve controller error handling 2020-10-16 11:31:31 +02:00
91ce7f7363 root: implement monitored tasks 2020-10-16 11:28:54 +02:00
17060238f0 build(deps): bump rollup in /passbook/static/static (#278)
Bumps [rollup](https://github.com/rollup/rollup) from 2.30.0 to 2.31.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.30.0...v2.31.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-16 10:01:01 +02:00
c392c2a74b build(deps): bump boto3 from 1.15.16 to 1.15.17 (#277)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.16 to 1.15.17.
- [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.15.16...1.15.17)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-16 09:39:12 +02:00
8cbaec8ba8 build(deps): bump kubernetes from 11.0.0 to 12.0.0 (#276)
Bumps [kubernetes](https://github.com/kubernetes-client/python) from 11.0.0 to 12.0.0.
- [Release notes](https://github.com/kubernetes-client/python/releases)
- [Changelog](https://github.com/kubernetes-client/python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes-client/python/compare/v11.0.0...v12.0.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-15 09:40:27 +02:00
4750f8c653 ci: fix typo 2020-10-14 20:28:24 +02:00
69d2a1cf3b providers/proxy: add more kubernetes tests 2020-10-14 20:21:47 +02:00
635f6c1ef2 ci: add k3d cluster for kubernetes controller tests 2020-10-14 20:21:36 +02:00
18da7565c2 outposts: improve performance by running related check in worker, fix tokens being left over on outpost delete 2020-10-14 18:41:16 +02:00
45699a1a69 outpost: rewrite kubernetes controller 2020-10-14 17:49:09 +02:00
5556e9f8e7 outposts: always save state, even without version 2020-10-14 12:15:40 +02:00
327bb09dd4 build(deps): bump rollup in /passbook/static/static (#275)
Bumps [rollup](https://github.com/rollup/rollup) from 2.29.0 to 2.30.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.29.0...v2.30.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-14 11:47:30 +02:00
8ca23451c6 outposts: rewrite state logic, use cache to expire old channels, support multiple instances 2020-10-14 11:32:33 +02:00
b99e2b10fe docs: add note about vcenter and AD Join 2020-10-14 11:32:33 +02:00
e966dff1a7 Revert "flows: rewrite shell to webcomponents"
This reverts commit b03a508475.
2020-10-14 11:32:33 +02:00
481fbedef2 build(deps): bump sentry-sdk from 0.18.0 to 0.19.0 (#274)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 0.18.0 to 0.19.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGES.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/0.18.0...0.19.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-14 11:27:35 +02:00
d104012eee build(deps-dev): bump colorama from 0.4.3 to 0.4.4 (#273)
Bumps [colorama](https://github.com/tartley/colorama) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/tartley/colorama/releases)
- [Changelog](https://github.com/tartley/colorama/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tartley/colorama/commits)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-13 11:55:05 +02:00
b03a508475 flows: rewrite shell to webcomponents 2020-10-12 17:53:35 +02:00
8ede4b6a13 build(deps): bump boto3 from 1.15.15 to 1.15.16 (#272)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.15 to 1.15.16.
- [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.15.15...1.15.16)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-12 10:32:52 +02:00
41323afccc docs: add upgrade instructions for 0.11 2020-10-11 23:42:38 +02:00
4a10b4999b core: fix navbar icon not showing in firefox 2020-10-11 23:38:22 +02:00
20ee634cda admin: add buttons to disable and enable users 2020-10-11 21:54:00 +02:00
713025d218 new release: 0.11.0-stable 2020-10-11 19:57:03 +02:00
58ae159835 outposts: disable Kubernetes selection for now 2020-10-11 19:40:22 +02:00
c95efe3cde docs: fix usage of user's groups 2020-10-11 19:29:22 +02:00
b6eb0bf53d providers/oauth2: add missing property_mapping template 2020-10-11 19:29:13 +02:00
610b6c7f70 policies: add PolicyAccessView, which does complete access checking 2020-10-11 19:26:20 +02:00
1ea2d99ff2 ci: run rollup build 2020-10-09 11:33:02 +02:00
67be43679c build(deps): bump boto3 from 1.15.14 to 1.15.15 (#268)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.14 to 1.15.15.
- [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.15.14...1.15.15)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-09 11:32:02 +02:00
fd42389bd5 build(deps-dev): bump rollup-plugin-sourcemaps (#267)
Bumps [rollup-plugin-sourcemaps](https://github.com/maxdavidson/rollup-plugin-sourcemaps) from 0.6.2 to 0.6.3.
- [Release notes](https://github.com/maxdavidson/rollup-plugin-sourcemaps/releases)
- [Changelog](https://github.com/maxdavidson/rollup-plugin-sourcemaps/blob/master/CHANGELOG.md)
- [Commits](https://github.com/maxdavidson/rollup-plugin-sourcemaps/compare/v0.6.2...v0.6.3)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-09 11:31:47 +02:00
71b1df2fec build(deps): bump rollup in /passbook/static/static (#269)
Bumps [rollup](https://github.com/rollup/rollup) from 2.28.2 to 2.29.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.28.2...v2.29.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-09 09:08:39 +02:00
7a3122f25c docs: add reverse-proxy example config, fix outpost docker-compose 2020-10-08 09:27:28 +02:00
63041d788b core: update application list API to show applications accessible by policy 2020-10-08 09:26:50 +02:00
bfc1bae0bb build(deps): bump boto3 from 1.15.13 to 1.15.14 (#266)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.13 to 1.15.14.
- [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.15.13...1.15.14)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-08 09:18:06 +02:00
8ab7f7fcbb core: make passbook title navigate to overview
closes #264
2020-10-07 19:27:20 +02:00
c1eb8317f7 providers/proxy: update phrasing for basic_auth_* attributes
closes #265
2020-10-07 19:27:06 +02:00
7a578e5e83 admin: dont show check when outpost hasnt connected
closes #263
2020-10-07 19:19:25 +02:00
b10912d8ba proxy: cleanup addHeadersForProxying 2020-10-07 18:02:57 +02:00
ef24b1cde2 docs: update screenshots 2020-10-07 18:02:26 +02:00
26cacc2a06 build(deps): bump boto3 from 1.15.12 to 1.15.13 (#259)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.12 to 1.15.13.
- [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.15.12...1.15.13)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-07 11:54:32 +02:00
ca0e89c799 build(deps): bump @patternfly/patternfly in /passbook/static/static (#261)
Bumps [@patternfly/patternfly](https://github.com/patternfly/patternfly) from 4.42.2 to 4.50.4.
- [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.42.2...prerelease-v4.50.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-07 09:10:47 +02:00
17950119ad build(deps): bump django-otp from 1.0.0 to 1.0.1 (#260)
Bumps [django-otp](https://github.com/django-otp/django-otp) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/django-otp/django-otp/releases)
- [Changelog](https://github.com/django-otp/django-otp/blob/master/CHANGES.rst)
- [Commits](https://github.com/django-otp/django-otp/compare/v1.0.0...v1.0.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-07 09:10:25 +02:00
876618c1ec build(deps): bump @fortawesome/fontawesome-free (#258)
Bumps [@fortawesome/fontawesome-free](https://github.com/FortAwesome/Font-Awesome) from 5.15.0 to 5.15.1.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/master/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/5.15.0...5.15.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-06 10:19:05 +02:00
2293ab69b9 build(deps): bump boto3 from 1.15.11 to 1.15.12 (#257)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.11 to 1.15.12.
- [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.15.11...1.15.12)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-06 09:55:56 +02:00
9df00e09a4 root: fix static docker's rollup build 2020-10-06 00:06:53 +02:00
cf6ce9c915 audit: optimize eventaction, 2020-10-05 23:43:56 +02:00
3b61191614 outpost: enable docker controller 2020-10-05 23:11:44 +02:00
9954eeac86 proxy: fix broken docker healthcheck 2020-10-05 22:53:26 +02:00
ac88bd5d44 core: hide token value by default 2020-10-05 22:40:30 +02:00
2406a619df root: fix lockfile in dockerfile 2020-10-05 22:37:53 +02:00
63087c9393 root: run backups in server contianer 2020-10-05 22:30:13 +02:00
da9aaf69df admin: add metrics and charts 2020-10-05 22:10:03 +02:00
ae125dd1f0 root: fix missing docker dependency 2020-10-04 15:04:07 +02:00
f636595230 static: add fetch-fill-slot to load data for admin interface 2020-10-04 13:09:03 +02:00
d506e8f1a3 outposts: implement docker controller 2020-10-04 00:41:12 +02:00
d3a96ac7aa outposts: load token async 2020-10-04 00:29:18 +02:00
189b0ec324 admin: expose info as API 2020-10-04 00:28:58 +02:00
c5a6b4961f core: Add Token identifier as sudo-primary key 2020-10-04 00:28:43 +02:00
b590589324 root: add base template for api 2020-10-03 23:20:33 +02:00
9fb1ac98ec Backup/Restore (#256)
* lifecycle: move s3 backup settings to s3 name

* providers/oauth2: fix for alerting for missing certificatekeypair

* lifecycle: add backup commands

see #252

* lifecycle: install postgres-client for 11 and 12

* root: migrate to DBBACKUP_STORAGE_OPTIONS, add region setting

* lifecycle: auto-clean last backups

* helm: add s3 region parameter, add cronjob for backups

* docs: add backup docs

* root: remove backup scheduled task for now
2020-10-03 20:36:36 +02:00
195d8fe71f core: move name field to base Provider 2020-10-03 20:05:16 +02:00
b0602a3215 admin: implement search for all views
see #253
2020-10-03 19:32:01 +02:00
0150a5c58c admin: add SearchListMixin mixin and partial template 2020-10-03 19:05:20 +02:00
b35d27c83e admin: fix pagination template, ensure template is placed correctly in footer 2020-10-03 17:50:17 +02:00
801bb90806 root: lock pyright version 2020-10-03 15:34:53 +02:00
55a83abb26 *: remove deprecated providing_args 2020-10-02 11:18:14 +02:00
c09b4e9713 e2e: fix invalid proxy image being pulled 2020-10-02 10:30:56 +02:00
247015e955 stages/otp_*: Remove duplicate validation for OTP Codes 2020-10-02 10:30:43 +02:00
fe3634be64 build(deps): bump django from 3.1.1 to 3.1.2 (#255)
Bumps [django](https://github.com/django/django) from 3.1.1 to 3.1.2.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.1...3.1.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-02 09:50:18 +02:00
ead20b03aa build(deps): bump boto3 from 1.15.9 to 1.15.10 (#254)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.9 to 1.15.10.
- [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.15.9...1.15.10)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-02 09:32:39 +02:00
932a475af7 docs: add notice about vcenter launch URL 2020-10-01 20:01:58 +02:00
e9a1a18ba3 providers/oauth2: ensure that when rs256 is selected, a certificate key pair is selected 2020-10-01 20:01:45 +02:00
6cd9edd38a providers/oauth2: add missing token_validity field to Forms and API 2020-10-01 20:01:28 +02:00
9b5f9167cd root: always enable dbbackup 2020-10-01 13:41:40 +02:00
1f30bcd335 root: lock postgresql to 12 in docker-compose 2020-10-01 10:42:38 +02:00
94eaeb5a60 new release: 0.10.9-stable 2020-10-01 10:24:16 +02:00
a5420fe019 providers/saml: lowercase acs URLs before checking
closes #249
2020-10-01 10:04:20 +02:00
2e1849a732 providers/oauth2: lowercase all uris before checking redirect URI
see #249
2020-10-01 10:00:44 +02:00
4039e96803 build(deps): bump boto3 from 1.15.8 to 1.15.9 (#250)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.8 to 1.15.9.
- [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.15.8...1.15.9)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-01 09:50:04 +02:00
8f585eca70 stages/identification: replace buggy FilteredSelectMultiple with ArrayFieldSelectMultiple 2020-09-30 23:58:01 +02:00
516455f482 stages/identification: add case_insensitive_matching
closes #248
2020-09-30 23:48:53 +02:00
719099a5af ci: remove deploy as --recreate is deprecated 2020-09-30 23:00:05 +02:00
7f74d32253 docs: update phrasing on tautulli docs 2020-09-30 21:19:04 +02:00
525d271535 *: apply new black styling 2020-09-30 19:34:22 +02:00
9ef39f1e04 root: update black version 2020-09-30 16:39:15 +02:00
9099dc5713 root: fix missing dependencies of uvicorn 2020-09-30 16:11:28 +02:00
c3c525a3f0 lib: re-add Websockets error 2020-09-30 15:55:59 +02:00
e699dfe88c ci: fix CD not working correctly 2020-09-30 15:41:04 +02:00
c0b334eb02 lib: ignore ChannelFull error 2020-09-30 15:40:54 +02:00
815ad26b91 root: add hard uvloop and httptools dependency 2020-09-30 15:37:15 +02:00
03647fa6af new release: 0.10.8-stable 2020-09-30 14:59:02 +02:00
5aec581585 docs: add docs for Tautulli
closes #244
2020-09-30 14:32:23 +02:00
68e9b7e140 proxy: only use logrus 2020-09-30 14:31:55 +02:00
b42bca4e3e build(deps): bump django-filter from 2.3.0 to 2.4.0 (#239)
Bumps [django-filter](https://github.com/carltongibson/django-filter) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/carltongibson/django-filter/releases)
- [Changelog](https://github.com/carltongibson/django-filter/blob/master/CHANGES.rst)
- [Commits](https://github.com/carltongibson/django-filter/compare/2.3.0...2.4.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-30 13:29:11 +02:00
42c9ac61b2 build(deps-dev): bump pytest from 6.0.2 to 6.1.0 (#238)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.0.2 to 6.1.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.0.2...6.1.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-30 13:14:42 +02:00
7cdc5f0568 build(deps): bump sentry-sdk from 0.17.8 to 0.18.0 (#245)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 0.17.8 to 0.18.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGES.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/0.17.8...0.18.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-30 12:44:50 +02:00
a063613f4c build(deps): bump uvicorn from 0.11.8 to 0.12.0 (#241)
* build(deps): bump uvicorn from 0.11.8 to 0.12.0

Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.11.8 to 0.12.0.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.11.8...0.12.0)

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

* lib: remove websockets ignored exception

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens.langhammer@beryju.org>
2020-09-30 11:49:59 +02:00
3af04bf1e4 build(deps): bump boto3 from 1.15.5 to 1.15.8 (#246)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.5 to 1.15.8.
- [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.15.5...1.15.8)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-30 11:49:36 +02:00
74f8b68af8 proxy: ask for pb_proxy scope, set authorization header if enabled 2020-09-30 11:49:06 +02:00
59dbc15be7 core: make group_attributes include user's attributes 2020-09-30 11:39:25 +02:00
9d5dd896f3 providers/proxy: start implementing basic_auth_enabled
see #244
2020-09-30 11:15:22 +02:00
02f5f12089 providers/proxy: use external_url for launch URL, hide setup URLs 2020-09-30 11:14:50 +02:00
90ea6dba90 providers/proxy: add pb_proxy scope for proxy that sends user_attributes 2020-09-30 11:13:59 +02:00
b0b2c0830b build(deps): bump github.com/sirupsen/logrus in /proxy (#243)
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.6.0...v1.7.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-30 09:32:33 +02:00
acb2b825f3 root: fix pipfile not referencing djangorestframework 2020-09-30 09:23:00 +02:00
e956b86649 root: lock rest-framework to 3.11.1 to prevent drf-yasg
See https://github.com/axnsan12/drf-yasg/issues/641
2020-09-30 09:15:48 +02:00
739c66da1c crypto: add tests 2020-09-30 09:12:37 +02:00
e8c7cce68f build(deps): bump @fortawesome/fontawesome-free (#247)
Bumps [@fortawesome/fontawesome-free](https://github.com/FortAwesome/Font-Awesome) from 5.14.0 to 5.15.0.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/master/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/5.14.0...5.15.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-30 08:57:47 +02:00
f741d382c2 e2e: bump wait 2020-09-29 15:01:01 +02:00
a13d4047b6 e2e: fix formatting 2020-09-29 14:04:23 +02:00
e0d8189442 e2e: update for new saml-test-sp, pull image before run 2020-09-29 12:47:38 +02:00
760352202e admin: fix get_form_class 2020-09-29 11:42:34 +02:00
9724ded194 policies: change .form() and .serializer() to properties, add tests 2020-09-29 10:32:58 +02:00
5da4ff4ff1 e2e: further cleanup tests, directly navigate to user-settings instead of click 2020-09-29 00:27:58 +02:00
e54b98a80e e2e: cleanup tests, remove XPATH selectors 2020-09-28 18:19:46 +02:00
67b69cb5d3 e2e: add oidc tests using oidc-test-client 2020-09-28 17:22:35 +02:00
863111ac57 e2e: fix oauth1 tests 2020-09-28 12:15:32 +02:00
bd78087582 root: fix RemovedInDjango40Warning being triggered 2020-09-28 11:47:50 +02:00
8f4e954160 providers/oauth2: rewrite introspection endpoint to allow basic or bearer auth 2020-09-28 11:42:27 +02:00
553f184aad e2e: add proxy connectivity test via Websocket 2020-09-28 09:04:44 +02:00
b6d7847eae providers/oauth2: fix token introspection view 2020-09-28 09:04:31 +02:00
ad0d339794 flows: add benchmark command 2020-09-27 21:21:30 +02:00
737cd22bb9 root: fix apt autoremove call 2020-09-27 21:07:29 +02:00
6ad1465f8f root: don't set default log level in docker-compose 2020-09-27 19:36:44 +02:00
d74fa4abbf admin: fix categories in sidebar being collapsible 2020-09-27 18:40:50 +02:00
b24938fc6b stages/consent: fix formatting 2020-09-26 21:06:01 +02:00
ea1564548c stages/consent: support pending_user from flow 2020-09-26 20:43:41 +02:00
3663c3c8a1 sources/saml: cleanup SLO Implementation 2020-09-26 20:38:38 +02:00
07e20a2950 core: add AuthJsonConsumer to handle websocket authentication 2020-09-26 20:11:04 +02:00
6366d50a0e core: show 'Create Application' button based on perms 2020-09-26 19:54:52 +02:00
c3e64df95b new release: 0.10.7-stable 2020-09-26 19:26:12 +02:00
d2bf2c8896 ci: fix prospector call 2020-09-26 19:17:42 +02:00
f27b43507c ci: ensure same checks as locally are run 2020-09-26 19:08:37 +02:00
c1058c7438 e2e: fix formatting 2020-09-26 18:18:01 +02:00
c37901feb9 e2e: add tests for oauth1 2020-09-26 17:44:05 +02:00
44b815efae sources/oauth: fix data being sent in body and header for oauth1 2020-09-26 17:43:58 +02:00
64a71a3663 flows: fix planner removing too many stages 2020-09-26 14:58:13 +02:00
ae435f423e ci: fix failing unittests not reporting correctly 2020-09-26 14:55:50 +02:00
7aa89c6d4f flows: fix formatting 2020-09-26 14:19:42 +02:00
7e9d7e5198 flows: fix two stages being removed when reevaluate_marker was enabled 2020-09-26 14:13:10 +02:00
2be6cd70d9 sources/oauth: fix handling of token for do_request 2020-09-26 14:00:48 +02:00
2b9705b33c policies/expression: remove pb_flow_plan, save flow context directly in context 2020-09-26 13:58:32 +02:00
502e43085f lifecycle: update celery command for 5.0 2020-09-26 02:17:39 +02:00
40f1de3b11 admin: load info about latest version in celery task 2020-09-26 02:16:35 +02:00
899c5b63ea admin: add BackSuccessUrlMixin to redirect to correct url after form edit 2020-09-26 02:04:16 +02:00
e104c74761 admin: make pagination size configurable 2020-09-26 01:55:40 +02:00
5d46c1ea5a flows: improve strings, ensure default-source-enrollment's first stage has re_evaluate_policies 2020-09-26 01:37:54 +02:00
7d533889bc sources/oauth: fix OAuth1 not working, cleanup 2020-09-26 01:27:33 +02:00
d9c2b32cba sources/oauth: cleanup clients, add type annotations 2020-09-26 00:34:57 +02:00
6e4ce8dbaa core: cache user's is_superuser 2020-09-26 00:34:35 +02:00
03d58b439f sources/oauth: separate clients into separate modules 2020-09-25 23:58:58 +02:00
ea38da441b ci: run e2e tests with failfast 2020-09-25 22:21:58 +02:00
bdaf0111c2 stages/password: fix formatting 2020-09-25 21:12:42 +02:00
974c2ddb11 stages/password: fix change_flow being deleted instead of renamed 2020-09-25 20:33:06 +02:00
769ce1c642 e2e: add tests for TOTP Setup, static OTP Setup and otp validation 2020-09-25 20:21:49 +02:00
f294791d41 stages/otp_time: fix redirect uri after setup 2020-09-25 19:39:19 +02:00
4ee22f8ec1 stages/otp_static: fix redirect URL after setup, fix stage not being passed to setup 2020-09-25 19:38:51 +02:00
74d3cfbba0 stages/otp_time: show OTP URI as aria-label 2020-09-25 19:03:12 +02:00
d278acb83b stages/otp_: fix flows having no title 2020-09-25 18:50:29 +02:00
84da454612 stages/otp_: ensure stage.configure_flow is set 2020-09-25 17:45:13 +02:00
52101007aa e2e: bump chrome version 2020-09-25 17:39:25 +02:00
dc57f433fd stages/password: update to use ConfigurableStage 2020-09-25 16:51:22 +02:00
3d4c5b8f4e stages/otp_time: implement configure_flow 2020-09-25 12:56:27 +02:00
e66424cc49 stages/otp_static: implement configure_flow 2020-09-25 12:56:14 +02:00
8fa83a8d08 flows: change setup_stage to configure_stage in migration 2020-09-25 12:55:33 +02:00
397892b282 stages/consent: cleanup 2020-09-25 12:49:19 +02:00
7be50c2574 flows: add ConfigurableStage base class and ConfigureFlowInitView 2020-09-25 12:49:19 +02:00
2aad523596 build(deps-dev): bump django-debug-toolbar from 3.1 to 3.1.1 (#236)
Bumps [django-debug-toolbar](https://github.com/jazzband/django-debug-toolbar) from 3.1 to 3.1.1.
- [Release notes](https://github.com/jazzband/django-debug-toolbar/releases)
- [Changelog](https://github.com/jazzband/django-debug-toolbar/blob/master/docs/changes.rst)
- [Commits](https://github.com/jazzband/django-debug-toolbar/compare/3.1...3.1.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-25 11:49:46 +02:00
6982b97eb0 build(deps): bump boto3 from 1.15.4 to 1.15.5 (#235)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.4 to 1.15.5.
- [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.15.4...1.15.5)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-25 10:51:57 +02:00
3de879496d build(deps): bump celery from 4.4.7 to 5.0.0 (#237)
Bumps [celery](https://github.com/celery/celery) from 4.4.7 to 5.0.0.
- [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/v4.4.7...v5.0.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-25 10:20:21 +02:00
4e75118a43 Create Dependabot config file (#234)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-25 09:36:29 +02:00
52c4fb431f core: add user.group_attributes 2020-09-24 15:45:58 +02:00
d696d854ff docs: update aws and gitlab docs 2020-09-24 15:36:29 +02:00
6966c119a7 build(deps): bump codemirror in /passbook/static/static (#231)
Bumps [codemirror](https://github.com/codemirror/CodeMirror) from 5.58.0 to 5.58.1.
- [Release notes](https://github.com/codemirror/CodeMirror/releases)
- [Changelog](https://github.com/codemirror/CodeMirror/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codemirror/CodeMirror/compare/5.58.0...5.58.1)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-24 14:31:49 +02:00
8cf5e647e3 build(deps): bump sentry-sdk from 0.17.7 to 0.17.8 (#229)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 0.17.7 to 0.17.8.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGES.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/0.17.7...0.17.8)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-24 14:21:39 +02:00
99bc6241f6 build(deps): bump boto3 from 1.15.3 to 1.15.4 (#230)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.3 to 1.15.4.
- [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.15.3...1.15.4)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-24 08:37:01 +02:00
e5f837ebb7 root: add issue templates 2020-09-23 14:05:36 +02:00
9d93da3d45 providers/proxy: fix formatting 2020-09-23 12:33:33 +02:00
9f6f18f9bb proxy: implement internal_host_ssl_validation option 2020-09-23 12:21:19 +02:00
6458b1dbf8 providers/proxy: make upstream SSL Validation configurable 2020-09-23 12:20:14 +02:00
1aff9afca6 build(deps): bump boto3 from 1.15.1 to 1.15.3 (#226)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.1 to 1.15.3.
- [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.15.1...1.15.3)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-23 11:51:02 +02:00
e0bc7d3932 build(deps): bump sentry-sdk from 0.17.6 to 0.17.7 (#228)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 0.17.6 to 0.17.7.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGES.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/0.17.6...0.17.7)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-23 08:51:50 +02:00
9fd9b2611c build(deps): bump codemirror in /passbook/static/static (#225)
Bumps [codemirror](https://github.com/codemirror/CodeMirror) from 5.57.0 to 5.58.0.
- [Release notes](https://github.com/codemirror/CodeMirror/releases)
- [Changelog](https://github.com/codemirror/CodeMirror/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codemirror/CodeMirror/compare/5.57.0...5.58.0)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-23 00:29:01 +02:00
6f3a1dfd08 build(deps-dev): bump django-debug-toolbar from 3.0 to 3.1 (#227)
Bumps [django-debug-toolbar](https://github.com/jazzband/django-debug-toolbar) from 3.0 to 3.1.
- [Release notes](https://github.com/jazzband/django-debug-toolbar/releases)
- [Changelog](https://github.com/jazzband/django-debug-toolbar/blob/master/docs/changes.rst)
- [Commits](https://github.com/jazzband/django-debug-toolbar/compare/3.0...3.1)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-22 23:54:09 +02:00
464b2cce88 audit: fix model information being saved nested 2020-09-21 22:34:03 +02:00
4eaa46e717 new release: 0.10.6-stable 2020-09-21 22:07:59 +02:00
59e8dca499 sources/ldap: divide connector into password, sync and auth, add unittests for password 2020-09-21 21:40:41 +02:00
945d5bfaf6 *: use Audit custom event action, add SOURCE_LINKED event action 2020-09-21 20:40:45 +02:00
dbcdab05ff audit: create audit logs for model creation/updating/deletion 2020-09-21 20:26:30 +02:00
e2cc2843d8 core: add X-passbook-id to every request with unique ID 2020-09-21 19:37:44 +02:00
241d59be8d ci: test migration from last released version to current branch (#224)
* ci: test migration test from last released version to current branch

* ci: fix typo

* ci: remove hyphens

* ci: checkout Build.SourceBranchName

* ci: attempt to fix Build.SourceBranchName

https://github.com/microsoft/azure-pipelines-tasks/issues/8793

* ci: fix duplicate variables entry

* ci: fix quoting for docker jobs

* ci: attempt to access branchName directly

* ci: attempt to extract branch name via sed

* ci: fix escaping for Build.SourceBranch

* ci: different bash substitution

* ci: replace /refs/pulls

* ci: attempt to save previous branch as variable

* ci: fix indent

* ci: try compile-time variables for docker

* ci: always use Build.SourceBranch

* ci: use compile-time template expression

* ci: use Build.SourceBranchName

* ci: attempt to get branch name from System.PullRequest.SourceBranch
2020-09-21 17:55:57 +02:00
74251a8883 audit: update swagger for event 2020-09-21 13:41:53 +02:00
585afd1bcd core: remove migration dependency on ldap 2020-09-21 13:21:03 +02:00
8358574484 audit: remove foreign key to user, save user data as json 2020-09-21 13:20:50 +02:00
cbcdaaf532 providers/oauth2: fix creation of new refresh token 2020-09-21 11:48:23 +02:00
f99eaa85ac sources/ldap: implement LDAP password validation and syncing 2020-09-21 11:46:35 +02:00
5007a6befe stages/prompt: integrate password comparison when multiple password fields are given 2020-09-21 11:04:31 +02:00
50c75087b8 lifecycle: fix startup logs not being full json 2020-09-21 11:04:31 +02:00
438e4efd49 build(deps-dev): bump django-debug-toolbar from 2.2 to 3.0 (#223)
Bumps [django-debug-toolbar](https://github.com/jazzband/django-debug-toolbar) from 2.2 to 3.0.
- [Release notes](https://github.com/jazzband/django-debug-toolbar/releases)
- [Changelog](https://github.com/jazzband/django-debug-toolbar/blob/master/docs/changes.rst)
- [Commits](https://github.com/jazzband/django-debug-toolbar/compare/2.2...3.0)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-21 10:59:43 +02:00
c7ca95ff2b new release: 0.10.5-stable 2020-09-20 13:58:33 +02:00
9f403a71ed root: fix IP detection when using multiple reverse proxies 2020-09-20 13:36:23 +02:00
2f4139df65 docs: add notice to use https when using external reverse proxy 2020-09-20 13:36:07 +02:00
f3ee8f7d9c admin: fix permissions not being checked for policybinding list 2020-09-19 23:07:39 +02:00
5fa3729702 audit: fix fields for events from impersonation being swapped 2020-09-19 22:54:36 +02:00
87f44fada4 providers/oauth2: fix refreshtoken being initialised wrong 2020-09-19 22:23:11 +02:00
c0026f3e16 admin: move pf-m-success to base css 2020-09-19 21:12:39 +02:00
c1051059f4 proxy: fix empty regex field being interpreted as regex 2020-09-19 21:05:41 +02:00
c25eda63ba new release: 0.10.4-stable 2020-09-19 19:40:58 +02:00
c90906c968 outposts: fix formatting 2020-09-19 19:12:49 +02:00
f6b52b9281 docs: add outpost upgrading docs 2020-09-19 19:04:04 +02:00
b04f92c8b4 admin: outposts show should-be version 2020-09-19 19:03:54 +02:00
a02fcb0a7a providers/oauth2: use # as separate for code#adfs, check if # exists in response_type and trim 2020-09-19 18:37:50 +02:00
c1ea605c7e build(deps): bump @patternfly/patternfly from 4.35.2 to 4.42.2 in /passbook/static/static (#222)
Bumps [@patternfly/patternfly](https://github.com/patternfly/patternfly) from 4.35.2 to 4.42.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.35.2...prerelease-v4.42.2)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-19 18:30:23 +02:00
116be0b3c0 sources/ldap: add status display to show last sync 2020-09-19 17:50:39 +02:00
438250b3a9 policies: improve wording on denied tempaltes 2020-09-19 15:24:52 +02:00
5e6acee2a5 root: increase limit of max-attributes in pylint 2020-09-19 13:40:23 +02:00
8b4222e7bb providers/proxy: fix formatting 2020-09-19 12:21:31 +02:00
4af563ce89 proxy: implement simple healthcheck 2020-09-19 11:43:22 +02:00
77842fab58 proxy: implement SkipAuthRegex 2020-09-19 11:32:21 +02:00
5689f25c39 providers/proxy: add option to skip authentication for paths matching regular expressions 2020-09-19 11:32:04 +02:00
a69c494feb stages/password: update swagger 2020-09-19 02:20:38 +02:00
83408b6ae0 stages/password: add failed_attempts_before_cancel to cancel a flow after x failed entries 2020-09-19 02:18:43 +02:00
d30abc64d0 flows: improve _full template being used for stage_invalid 2020-09-19 02:15:15 +02:00
6674d3e017 e2e: fix tests for proxy provider/outpost 2020-09-19 01:54:54 +02:00
4749c3fad0 proxy: improve reconnect logic, send version, properly version proxy 2020-09-19 01:37:08 +02:00
18886697d6 outposts: add support for version checking 2020-09-19 01:34:11 +02:00
e75c9e9a79 providers/oauth2: make openid-configuration easily readable 2020-09-19 01:34:11 +02:00
5a3c1137ab providers/oauth2: add more info to configuration modal 2020-09-19 01:34:11 +02:00
ddca46e24a outposts: add modal to show setup information 2020-09-19 01:34:11 +02:00
22a9abf7bf docs: add docs for sonarr 2020-09-19 01:34:11 +02:00
fb16502466 build(deps): bump boto3 from 1.14.63 to 1.15.1 (#221)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-19 00:46:09 +02:00
421bd13ddf admin: make YAMLField return empty dict when empty yaml is given 2020-09-19 00:00:55 +02:00
404c9ef753 providers/saml: improve __str__ of SAMLPropertyMapping 2020-09-18 23:50:31 +02:00
a57b545093 docs: add landscape integration 2020-09-18 23:50:16 +02:00
d8530f238d docs: update sentry and awx integrations 2020-09-18 23:50:00 +02:00
fe4a0c3b44 core: add impersonation start/end to audit log
also add impersonated user as context to other logs
2020-09-18 23:39:37 +02:00
e0c104ee5c providers/oauth2: remove post_logout_redirect_uris 2020-09-18 23:37:40 +02:00
6ab8794754 proxy: improve logging and reconnecting 2020-09-18 21:54:23 +02:00
316e6cb17f admin: set default host for outposts based on HTTP host 2020-09-18 21:51:08 +02:00
9d5d99290c outposts: only show proxy providers 2020-09-18 21:50:49 +02:00
20ffe833de admin: fix create link for outposts 2020-09-18 21:28:48 +02:00
d4d026bf6a stages/user_write: add migration that removes unintended data 2020-09-18 18:58:07 +02:00
dfe093b2b9 stages/user_write: fix unittests 2020-09-18 18:52:19 +02:00
60739e620e stages/user_write: fix formatting 2020-09-18 18:41:11 +02:00
d6cc6770b8 stages/user_write: fix data being saved as attributes without intent 2020-09-18 18:15:33 +02:00
ddc1022461 stages/user_write: check if session hash should be updated early 2020-09-18 18:15:25 +02:00
2c2226610e providers/oauth2: fix end-session view not working, add tests 2020-09-17 21:55:01 +02:00
cba78b4de7 providers/*: fix launch_url not working 2020-09-17 21:53:57 +02:00
1eeb64ee39 docs: fix environment variable for error reporting 2020-09-17 21:22:46 +02:00
22dea62084 root: fix startup log not showing in docker 2020-09-17 21:16:31 +02:00
5ff1dd8426 core: move impersonation to core, add tests, add better permission checks 2020-09-17 16:24:53 +02:00
da15a8878f stages/password: improve labelling of LDAP backend 2020-09-17 15:54:48 +02:00
bf33828ac1 core: fix overview template for non-rectangular icons 2020-09-17 10:44:10 +02:00
950a1fc77e docs: add vsphere note for code (ADFS) 2020-09-17 10:43:59 +02:00
895e7d7393 new release: 0.10.3-stable 2020-09-17 10:10:39 +02:00
3beca0574d build(deps): bump github.com/sirupsen/logrus in /proxy (#219)
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.4.2 to 1.6.0.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.4.2...v1.6.0)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-17 10:01:23 +02:00
990f5f0a43 proxy: bump versions 2020-09-17 09:35:16 +02:00
97ce143efe lifecycle: adjust worker count 2020-09-17 09:35:08 +02:00
cbbe174fd8 build(deps): bump boto3 from 1.14.62 to 1.14.63 (#218)
Bumps [boto3](https://github.com/boto/boto3) from 1.14.62 to 1.14.63.
- [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.14.62...1.14.63)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-17 08:57:57 +02:00
da3c640343 admin: fix type annotation for latest_version() 2020-09-16 23:54:55 +02:00
4b39c71de0 providers/oauth2: accept token as post param 2020-09-16 23:38:55 +02:00
818f417fd8 providers/oauth2: only send id_token as access_token if ADFS compat mode is enabled 2020-09-16 23:31:03 +02:00
f1ccef7f6a e2e: add tests for proxy provider and outposts 2020-09-16 23:22:17 +02:00
6187436518 build(deps): bump sentry-sdk from 0.17.4 to 0.17.6 (#217)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 0.17.4 to 0.17.6.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGES.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/0.17.4...0.17.6)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-16 10:30:19 +02:00
9559ee7cb9 build(deps-dev): bump pytest-django from 3.9.0 to 3.10.0 (#215)
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 3.9.0 to 3.10.0.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/master/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v3.9.0...v3.10.0)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-16 10:08:25 +02:00
72e9c4e6fa build(deps): bump boto3 from 1.14.60 to 1.14.62 (#216)
Bumps [boto3](https://github.com/boto/boto3) from 1.14.60 to 1.14.62.
- [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.14.60...1.14.62)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-16 09:44:23 +02:00
97b8a025b3 ci: cleanup containers post e2e job 2020-09-16 09:08:10 +02:00
ea9687c30b core: don't fail migrations if no pbadmin exists 2020-09-15 23:37:39 +02:00
0a5e14a352 core: make is_superuser a group property, remove from user 2020-09-15 23:10:31 +02:00
0325847c22 docs: add vmware vsphere integration doc 2020-09-15 21:51:27 +02:00
491dcc1159 sources/ldap: improve default Property Mappings 2020-09-15 21:51:08 +02:00
6292049c74 sources/ldap: add limited support for attributes as object_fields on LDAPPropertyMappings 2020-09-15 21:08:14 +02:00
1e97af772f providers/oauth2: add workaround for vcenter 2020-09-15 20:54:54 +02:00
5c622cd4d2 providers/oauth2: make sub configurable based on hash, username, email and upn 2020-09-15 20:54:42 +02:00
c4de808c4e readme: link to install instructions from docs 2020-09-15 17:17:29 +02:00
8c604d225b static: update flow background 2020-09-15 16:14:13 +02:00
c7daadfb18 core: fix logic error in expired models cleanup 2020-09-15 12:53:02 +02:00
683968c96e sources/ldap: register ldap sources 2020-09-15 12:36:33 +02:00
c94added99 helm: bump dependency versions 2020-09-15 12:36:09 +02:00
61c00e5b39 ci: create release as draft 2020-09-15 12:36:02 +02:00
566ebae065 new release: 0.10.2-stable 2020-09-15 12:04:00 +02:00
9b62a6403b helm: fix affinity rules and resources 2020-09-15 11:41:11 +02:00
8c465b2026 outposts: remove unused import 2020-09-15 11:32:25 +02:00
6b7da71aa8 lib: improve error handling for sentry 2020-09-15 11:29:43 +02:00
e95bbfab9a outposts: disable WIP k8s controller 2020-09-15 11:25:59 +02:00
e401575894 lifecycle: fix worker not running scheduled tasks 2020-09-15 11:20:28 +02:00
6428801270 e2e: update e2e tests for new AccessDenied response 2020-09-15 10:30:04 +02:00
3e13c13619 flows: replace passbook_flows:denied with AccessDenied Reeponse 2020-09-15 09:54:19 +02:00
92f79eb30e policies: add AccessDeniedResponse as general response when access was denied 2020-09-15 09:53:59 +02:00
e7472de4bf sources/ldap: sync source on save 2020-09-14 23:35:01 +02:00
494950ac65 admin: fix anonymous user not being removed from user count 2020-09-14 23:19:16 +02:00
4d51295db2 new release: 0.10.1-stable 2020-09-14 23:08:57 +02:00
3bbded3555 docs: remove default password for docker-compose, improve instructions 2020-09-14 23:08:04 +02:00
b3262e2a82 docs: add docs for passbook_user_debug 2020-09-14 22:51:50 +02:00
40614a65fc flows: move complete denied view and template to flows 2020-09-14 21:52:43 +02:00
3cf558d594 providers/*: pass policy result objects when access denied 2020-09-14 21:52:25 +02:00
812cc0d2f1 policies: add references for source_policy and source_results 2020-09-14 21:51:59 +02:00
e21ed92848 providers/oauth2: ensure flow is cleaned up on error 2020-09-14 18:40:44 +02:00
5184c4b7ef flows: fix FlowNonApplicableException and EmptyFlowException leading to infinite spinners 2020-09-14 18:40:26 +02:00
2c07859b68 core: add automatic launch_url detection based on provider 2020-09-14 18:12:42 +02:00
ae6304c05e providers/proxy: fix provider requiring a certificate to be selected 2020-09-14 17:37:06 +02:00
501683e3cb outposts: add tests for permissions 2020-09-14 17:34:07 +02:00
cc8afa8706 admin: don't show policy as unbound when used as validation policy 2020-09-14 15:44:33 +02:00
17a9e02bc0 docs: update kubernetes deployment example 2020-09-14 15:41:24 +02:00
6a669992a8 outposts: fix permissions not being updated when providers are modified 2020-09-14 15:41:02 +02:00
7ea5c22b6c root: fix channels not loading redis connection details 2020-09-14 14:21:43 +02:00
b11d6a5891 build(deps): bump django-storages from 1.10 to 1.10.1 (#212)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Jens L <jens@beryju.org>
2020-09-14 10:29:49 +02:00
49830367a7 build(deps-dev): bump coverage from 5.2.1 to 5.3 (#213)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Jens L <jens@beryju.org>
2020-09-14 10:26:25 +02:00
e69ca5a229 ci: fix coverage combine for unittest and e2e 2020-09-14 09:52:43 +02:00
a57d21f5e8 build(deps): bump boto3 from 1.14.59 to 1.14.60 (#210)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Jens L <jens@beryju.org>
2020-09-14 09:09:34 +02:00
c7026407c6 policies: fix type error 2020-09-14 00:28:23 +02:00
69eecd6b60 helm: add soft-affinity rules for worker and web 2020-09-14 00:12:40 +02:00
810f10edfe providers/oauth2: fix several small implicit flow errors 2020-09-14 00:11:11 +02:00
1c57128f11 providers/oauth2: fix token to code_token 2020-09-13 23:42:45 +02:00
82eade3eb1 new release: 0.10.0-stable 2020-09-13 23:03:38 +02:00
56a9dcc88d ci: fix CI trying to run e2e tests 2020-09-13 23:02:46 +02:00
fe70d80189 docs: fix kubernetes values version 2020-09-13 22:31:42 +02:00
e97e22c58a root: fix readme image link 2020-09-13 22:27:26 +02:00
bb4e39aab6 docs: add outpost deployment docs, link in outposts list 2020-09-13 22:20:17 +02:00
a8744f443c outposts: fix Kubernetes Controller not exporting dicts, secrets not being b64 encoded 2020-09-13 22:19:26 +02:00
7fe9b8f0b4 providers/proxy: add domainless URL Validator 2020-09-13 21:52:34 +02:00
696aa7e5f6 core: fix path to default icon 2020-09-13 20:47:17 +02:00
e1d82aee1d ci: run e2e tests on custom agent 2020-09-13 19:49:13 +02:00
151374f565 stages/email: fix loading of static files when path is a directory 2020-09-13 18:24:49 +02:00
bebeff9f7f root: allow for changing of logo and branding 2020-09-13 17:52:33 +02:00
8b99afa34d stages/email: fix binary files not being encoded correctly 2020-09-13 17:40:13 +02:00
b317852e8a static: replace brand.svg with text and font 2020-09-13 17:33:30 +02:00
24ae35c35a build(deps-dev): bump pytest from 6.0.1 to 6.0.2 (#211)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.0.1 to 6.0.2.
- [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.0.1...6.0.2)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-13 16:01:23 +02:00
8e6bb48227 sources/saml: add mitigation for idp-initiated requests 2020-09-13 15:39:25 +02:00
7a4e8af1ae outpost: fix outpost update signal only being sent to outposts connected to the same passbook instance 2020-09-13 14:29:40 +02:00
0161205c82 sources/saml: fix previous request ID being wrongly compared
request ID was compared to request ID not InResponseTo field
2020-09-13 14:00:56 +02:00
ca0ba85023 providers/saml: disallow idp-initiated SSO by default and validate Request ID 2020-09-12 00:53:44 +02:00
c2ebaa7f64 e2e: add oauth source test case with SameSite strict 2020-09-11 23:54:20 +02:00
23cccebb96 pytest (#209) 2020-09-11 23:21:11 +02:00
3f5d30e6fe build(deps): bump boto3 from 1.14.58 to 1.14.59 (#208)
Bumps [boto3](https://github.com/boto/boto3) from 1.14.58 to 1.14.59.
- [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.14.58...1.14.59)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-11 08:53:42 +02:00
ca735349f9 proxy: fix listening on wrong ip 2020-09-10 21:13:26 +02:00
25ce8c6dc7 build(deps): bump boto3 from 1.14.56 to 1.14.58 (#206)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Jens L <jens@beryju.org>
2020-09-10 18:28:22 +02:00
081ac0bcdb root/asgi: hide healthcheck logs from sentry 2020-09-10 17:29:13 +02:00
8a07b349ee root: fix IP detection in ASGI logger, attempt to fix out of order issues 2020-09-10 16:58:25 +02:00
b3468bc265 providers/oauth2: fix comparison to undefined ResponseTypes 2020-09-10 16:26:55 +02:00
4edfad869f helm: fix missing .Values prefix for replicas 2020-09-10 15:07:56 +02:00
404f5d7912 new release: 0.10.0-rc6 2020-09-10 14:35:17 +02:00
8bea99a953 ci: run on release publish and creation 2020-09-10 14:35:13 +02:00
0b0ba33dce new release: 0.10.0-rc5 2020-09-10 14:24:31 +02:00
e3627b2cd9 ci: generate proxy api client before building docker image 2020-09-10 14:24:02 +02:00
37fac3ae00 ci: fix release being run on release edit 2020-09-10 13:25:08 +02:00
501 changed files with 13669 additions and 4370 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.10.0-rc4
current_version = 0.12.2-stable
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -15,10 +15,10 @@ values =
beta
stable
[bumpversion:file:README.md]
[bumpversion:file:docs/installation/docker-compose.md]
[bumpversion:file:docs/installation/kubernetes.md]
[bumpversion:file:docker-compose.yml]
[bumpversion:file:helm/values.yaml]
@ -28,3 +28,5 @@ values =
[bumpversion:file:.github/workflows/release.yml]
[bumpversion:file:passbook/__init__.py]
[bumpversion:file:proxy/pkg/version.go]

View File

@ -1,5 +1,6 @@
[run]
source = passbook
relative_files = true
omit =
*/asgi.py
manage.py

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,34 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Output of docker-compose logs or kubectl logs respectively
**Version and Deployment (please complete the following information):**
- passbook version: [e.g. 0.10.0-stable]
- Deployment: [e.g. docker-compose, helm]
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

26
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,26 @@
version: 2
updates:
- package-ecosystem: gomod
directory: "/proxy"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
assignees:
- BeryJu
- package-ecosystem: npm
directory: "/passbook/static/static"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
assignees:
- BeryJu
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
assignees:
- BeryJu

View File

@ -1,54 +0,0 @@
name: "CodeQL"
on:
push:
branches: [master, admin-more-info, ci-deploy-dev, gh-pages, provider-saml-v2]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 20 * * 2'
jobs:
analyse:
name: Analyse
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -1,6 +1,8 @@
name: passbook-release
name: passbook-on-release
on:
release
release:
types: [published, created]
jobs:
# Build
@ -16,17 +18,26 @@ jobs:
- name: Building Docker Image
run: docker build
--no-cache
-t beryju/passbook:0.10.0-rc4
-t beryju/passbook:0.12.2-stable
-t beryju/passbook:latest
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook:0.10.0-rc4
run: docker push beryju/passbook:0.12.2-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook:latest
build-proxy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-go@v2
with:
go-version: "^1.15"
- name: prepare go api client
run: |
cd proxy
go get -u github.com/go-swagger/go-swagger/cmd/swagger
swagger generate client -f ../swagger.yaml -A passbook -t pkg/
go build -v .
- name: Docker Login Registry
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
@ -37,11 +48,11 @@ jobs:
cd proxy
docker build \
--no-cache \
-t beryju/passbook-proxy:0.10.0-rc4 \
-t beryju/passbook-proxy:0.12.2-stable \
-t beryju/passbook-proxy:latest \
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-proxy:0.10.0-rc4
run: docker push beryju/passbook-proxy:0.12.2-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-proxy:latest
build-static:
@ -66,11 +77,11 @@ jobs:
run: docker build
--no-cache
--network=$(docker network ls | grep github | awk '{print $1}')
-t beryju/passbook-static:0.10.0-rc4
-t beryju/passbook-static:0.12.2-stable
-t beryju/passbook-static:latest
-f static.Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-static:0.10.0-rc4
run: docker push beryju/passbook-static:0.12.2-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-static:latest
test-release:
@ -82,10 +93,13 @@ jobs:
- uses: actions/checkout@v1
- name: Run test suite in final docker images
run: |
sudo apt-get install -y pwgen
echo "PG_PASS=$(pwgen 40 1)" >> .env
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
docker-compose pull -q
docker-compose up --no-start
docker-compose start postgresql redis
docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"
docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test passbook"
sentry-release:
needs:
- test-release
@ -100,5 +114,5 @@ jobs:
SENTRY_PROJECT: passbook
SENTRY_URL: https://sentry.beryju.org
with:
tagName: 0.10.0-rc4
tagName: 0.12.2-stable
environment: beryjuorg-prod

View File

@ -1,10 +1,10 @@
name: passbook-on-tag
on:
push:
tags:
- 'version/*'
name: passbook-version-tag
jobs:
build:
name: Create Release from Tag
@ -13,7 +13,10 @@ jobs:
- uses: actions/checkout@master
- name: Pre-release test
run: |
export PASSBOOK_TAG=latest
sudo apt-get install -y pwgen
echo "PASSBOOK_TAG=latest" >> .env
echo "PG_PASS=$(pwgen 40 1)" >> .env
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
docker-compose pull -q
docker build \
--no-cache \
@ -21,7 +24,7 @@ jobs:
-f Dockerfile .
docker-compose up --no-start
docker-compose start postgresql redis
docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"
docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test passbook"
- name: Install Helm
run: |
apt update && apt install -y curl
@ -31,7 +34,7 @@ jobs:
helm dependency update helm/
helm package helm/
mv passbook-*.tgz passbook-chart.tgz
- name: Extract verison number
- name: Extract version number
id: get_version
uses: actions/github-script@0.2.0
with:
@ -46,7 +49,7 @@ jobs:
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ steps.get_version.outputs.result }}
draft: false
draft: true
prerelease: false
- name: Upload packaged Helm Chart
id: upload-release-asset

View File

@ -1,9 +1,16 @@
[MASTER]
disable=arguments-differ,no-self-use,fixme,locally-disabled,too-many-ancestors,too-few-public-methods,import-outside-toplevel,bad-continuation,signature-differs,similarities,cyclic-import
load-plugins=pylint_django,pylint.extensions.bad_builtin
extension-pkg-whitelist=lxml
# Allow constants to be shorter than normal (and lowercase, for settings.py)
const-rgx=[a-zA-Z0-9_]{1,40}$
ignored-modules=django-otp
jobs=12
ignore=migrations
max-attributes=12
jobs=12

View File

@ -16,11 +16,15 @@ COPY --from=locker /app/requirements.txt /
COPY --from=locker /app/requirements-dev.txt /
RUN apt-get update && \
apt-get install -y --no-install-recommends postgresql-client-11 build-essential && \
rm -rf /var/lib/apt/ && \
pip install -r /requirements.txt --no-cache-dir && \
apt-get install -y --no-install-recommends curl ca-certificates gnupg && \
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
echo "deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
apt-get update && \
apt-get install -y --no-install-recommends postgresql-client-12 postgresql-client-11 build-essential && \
apt-get clean && \
pip install -r /requirements.txt --no-cache-dir && \
apt-get remove --purge -y build-essential && \
apt-get autoremove --purge && \
apt-get autoremove --purge -y && \
adduser --system --no-create-home --uid 1000 --group --home /passbook passbook
COPY ./passbook/ /passbook

View File

@ -1,20 +1,26 @@
all: lint-fix lint coverage gen
coverage:
coverage run --concurrency=multiprocessing manage.py test passbook --failfast
coverage run --concurrency=multiprocessing manage.py test --failfast -v 3
coverage combine
coverage html
coverage report
lint-fix:
isort -rc .
black .
black passbook e2e lifecycle
lint:
pyright
bandit -r .
pylint passbook
pyright passbook e2e lifecycle
bandit -r passbook e2e lifecycle
pylint passbook e2e lifecycle
prospector
gen: coverage
./manage.py generate_swagger -o swagger.yaml -f yaml
local-stack:
export PASSBOOK_TAG=testing
docker build -t beryju/passbook:testng .
docker-compose up -d
docker-compose run --rm server migrate

13
Pipfile
View File

@ -17,10 +17,10 @@ django-otp = "*"
django-prometheus = "*"
django-recaptcha = "*"
django-redis = "*"
django-rest-framework = "*"
djangorestframework = "*"
django-storages = "*"
djangorestframework-guardian = "*"
drf-yasg = "*"
drf_yasg2 = "*"
facebook-sdk = "*"
ldap3 = "*"
lxml = "*"
@ -28,7 +28,7 @@ packaging = "*"
psycopg2-binary = "*"
pycryptodome = "*"
pyjwkest = "*"
uvicorn = "*"
uvicorn = {extras = ["standard"],version = "*"}
gunicorn = "*"
pyyaml = "*"
qrcode = "*"
@ -43,6 +43,7 @@ dacite = "*"
channels = "*"
channels-redis = "*"
kubernetes = "*"
docker = "*"
[requires]
python_version = "3.8"
@ -50,14 +51,14 @@ python_version = "3.8"
[dev-packages]
autopep8 = "*"
bandit = "*"
black = "==19.10b0"
black = "==20.8b1"
bumpversion = "*"
colorama = "*"
coverage = "*"
django-debug-toolbar = "*"
docker = "*"
pylint = "*"
pylint-django = "*"
selenium = "*"
unittest-xml-reporting = "*"
prospector = "*"
pytest = "*"
pytest-django = "*"

695
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
<img src="passbook/static/static/passbook/logo.svg" height="50" alt="passbook logo"><img src="passbook/static/static/passbook/brand_inverted.svg" height="50" alt="passbook">
<img src="docs/images/logo.svg" height="50" alt="passbook logo"><img src="docs/images/brand_inverted.svg" height="50" alt="passbook">
[![CI Build status](https://img.shields.io/azure-devops/build/beryjuorg/passbook/1?style=flat-square)](https://dev.azure.com/beryjuorg/passbook/_build?definitionId=1)
![Tests](https://img.shields.io/azure-devops/tests/beryjuorg/passbook/1?compact_message&style=flat-square)
@ -13,20 +13,7 @@ passbook is an open-source Identity Provider focused on flexibility and versatil
## Installation
For small/test setups it is recommended to use docker-compose.
```
wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
# Optionally enable Error-reporting
# export PASSBOOK_ERROR_REPORTING=true
# Optionally deploy a different version
# export PASSBOOK_TAG=0.10.0-rc4
# If this is a productive installation, set a different PostgreSQL Password
# export PG_PASS=$(pwgen 40 1)
docker-compose pull
docker-compose up -d
docker-compose run --rm server migrate
```
For small/test setups it is recommended to use docker-compose, see the [documentation](https://passbook.beryju.org/installation/docker-compose/)
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://passbook.beryju.org//installation/kubernetes/)

View File

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

View File

@ -8,6 +8,10 @@ variables:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
${{ if startsWith(variables['Build.SourceBranch'], 'refs/heads/') }}:
branchName: ${{ replace(variables['Build.SourceBranchName'], 'refs/heads/', '') }}
${{ if startsWith(variables['Build.SourceBranch'], 'refs/pull/') }}:
branchName: ${{ replace(variables['System.PullRequest.SourceBranch'], 'refs/heads/', '') }}
stages:
- stage: Lint
@ -26,7 +30,7 @@ stages:
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run pylint passbook
script: pipenv run pylint passbook e2e lifecycle
- job: black
pool:
vmImage: 'ubuntu-latest'
@ -41,7 +45,7 @@ stages:
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run black --check passbook
script: pipenv run black --check passbook e2e lifecycle
- job: prospector
pool:
vmImage: 'ubuntu-latest'
@ -57,7 +61,7 @@ stages:
pipenv install --dev prospector --skip-lock
- task: CmdLine@2
inputs:
script: pipenv run prospector passbook
script: pipenv run prospector
- job: bandit
pool:
vmImage: 'ubuntu-latest'
@ -72,7 +76,7 @@ stages:
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run bandit -r passbook
script: pipenv run bandit -r passbook e2e lifecycle
- job: pyright
pool:
vmImage: ubuntu-latest
@ -85,7 +89,7 @@ stages:
versionSpec: '3.8'
- task: CmdLine@2
inputs:
script: npm install -g pyright
script: npm install -g pyright@1.1.79
- task: CmdLine@2
inputs:
script: |
@ -93,7 +97,7 @@ stages:
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run pyright
script: pipenv run pyright e2e lifecycle
- stage: Test
jobs:
- job: migrations
@ -117,6 +121,41 @@ stages:
- task: CmdLine@2
inputs:
script: pipenv run ./manage.py migrate
- job: migrations_from_previous_release
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/ci.docker-compose.yml'
action: 'Run services'
buildImages: false
- task: CmdLine@2
displayName: Prepare Last tagged release
inputs:
script: |
git checkout $(git describe --abbrev=0 --match 'version/*')
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
displayName: Migrate to last tagged release
inputs:
script: pipenv run ./manage.py migrate
- task: CmdLine@2
displayName: Install current branch
inputs:
script: |
set -x
git checkout ${{ variables.branchName }}
pipenv sync --dev
- task: CmdLine@2
displayName: Migrate to current branch
inputs:
script: pipenv run ./manage.py migrate
- job: coverage_unittest
pool:
vmImage: 'ubuntu-latest'
@ -130,6 +169,13 @@ stages:
dockerComposeFile: 'scripts/ci.docker-compose.yml'
action: 'Run services'
buildImages: false
- task: CmdLine@2
displayName: Install K3d and prepare
inputs:
script: |
wget -q -O - https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash
k3d cluster create
k3d kubeconfig write -o ~/.kube/config --overwrite
- task: CmdLine@2
inputs:
script: |
@ -139,7 +185,11 @@ stages:
displayName: Run full test suite
inputs:
script: |
pipenv run coverage run ./manage.py test passbook
export PB_TEST_K8S=true
pipenv run coverage run ./manage.py test passbook -v 3
- task: CmdLine@2
inputs:
script: |
mkdir output-unittest
mv unittest.xml output-unittest/unittest.xml
mv .coverage output-unittest/coverage
@ -150,7 +200,7 @@ stages:
publishLocation: 'pipeline'
- job: coverage_e2e
pool:
vmImage: 'ubuntu-latest'
name: coventry
steps:
- task: UsePythonVersion@0
inputs:
@ -161,6 +211,13 @@ stages:
dockerComposeFile: 'scripts/ci.docker-compose.yml'
action: 'Run services'
buildImages: false
- task: CmdLine@2
displayName: Install K3d and prepare
inputs:
script: |
wget -q -O - https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash
k3d cluster create
k3d kubeconfig write -o ~/.kube/config --overwrite
- task: CmdLine@2
inputs:
script: |
@ -177,11 +234,21 @@ stages:
inputs:
script: |
cd passbook/static/static
yarn
npm i
npm run build
- task: CmdLine@2
displayName: Run full test suite
inputs:
script: pipenv run coverage run ./manage.py test e2e
script: |
export PB_TEST_K8S=true
pipenv run coverage run ./manage.py test e2e -v 3 --failfast
- task: CmdLine@2
condition: always()
displayName: Cleanup
inputs:
script: |
docker stop $(docker ps -aq)
docker container prune -f
- task: CmdLine@2
displayName: Prepare unittests and coverage for upload
inputs:
@ -225,11 +292,9 @@ stages:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
find .
pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage
pipenv run coverage xml
pipenv run coverage html
find .
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: 'Cobertura'
@ -260,7 +325,7 @@ stages:
repository: 'beryju/passbook'
command: 'buildAndPush'
Dockerfile: 'Dockerfile'
tags: 'gh-$(Build.SourceBranchName)'
tags: "gh-${{ variables.branchName }}"
- job: build_static
pool:
vmImage: 'ubuntu-latest'
@ -277,27 +342,11 @@ stages:
repository: 'beryju/passbook-static'
command: 'build'
Dockerfile: 'static.Dockerfile'
tags: 'gh-$(Build.SourceBranchName)'
tags: "gh-${{ variables.branchName }}"
arguments: "--network=beryjupassbook_default"
- task: Docker@2
inputs:
containerRegistry: 'dockerhub'
repository: 'beryju/passbook-static'
command: 'push'
tags: 'gh-$(Build.SourceBranchName)'
- stage: Deploy
jobs:
- job: deploy_dev
pool:
vmImage: 'ubuntu-latest'
steps:
- task: HelmDeploy@0
inputs:
connectionType: 'Kubernetes Service Connection'
kubernetesServiceConnection: 'k8s-beryjuorg-prd'
namespace: 'passbook-dev'
command: 'upgrade'
chartType: 'FilePath'
chartPath: 'helm/'
releaseName: 'passbook-dev'
recreate: true
tags: "gh-${{ variables.branchName }}"

View File

@ -3,7 +3,7 @@ version: '3.2'
services:
postgresql:
image: postgres
image: postgres:12
volumes:
- database:/var/lib/postgresql/data
networks:
@ -12,55 +12,68 @@ services:
- POSTGRES_PASSWORD=${PG_PASS:-thisisnotagoodpassword}
- POSTGRES_USER=passbook
- POSTGRES_DB=passbook
labels:
- traefik.enable=false
env_file:
- .env
redis:
image: redis
networks:
- internal
labels:
- traefik.enable=false
server:
image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-rc4}
image: beryju/passbook:${PASSBOOK_TAG:-0.12.2-stable}
command: server
environment:
PASSBOOK_REDIS__HOST: redis
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
PASSBOOK_POSTGRESQL__HOST: postgresql
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword}
PASSBOOK_LOG_LEVEL: debug
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
ports:
- 8000
networks:
- internal
labels:
- traefik.port=8000
- traefik.docker.network=internal
- traefik.frontend.rule=PathPrefix:/
traefik.enable: 'true'
traefik.docker.network: internal
traefik.http.routers.app-router.rule: PathPrefix(`/`)
traefik.http.routers.app-router.service: app-service
traefik.http.routers.app-router.tls: 'true'
traefik.http.services.app-service.loadbalancer.healthcheck.hostname: passbook-healthcheck-host
traefik.http.services.app-service.loadbalancer.server.port: '8000'
env_file:
- .env
worker:
image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-rc4}
image: beryju/passbook:${PASSBOOK_TAG:-0.12.2-stable}
command: worker
networks:
- internal
labels:
- traefik.enable=false
environment:
PASSBOOK_REDIS__HOST: redis
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
PASSBOOK_POSTGRESQL__HOST: postgresql
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword}
PASSBOOK_LOG_LEVEL: debug
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
volumes:
- ./backups:/backups
- /var/run/docker.socket:/var/run/docker.socket
env_file:
- .env
static:
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.0-rc4}
image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.2-stable}
networks:
- internal
labels:
- traefik.frontend.rule=PathPrefix:/static, /robots.txt, /favicon.ico
- traefik.port=80
- traefik.docker.network=internal
traefik.enable: 'true'
traefik.docker.network: internal
traefik.http.routers.static-router.rule: PathPrefix(`/static`, `/robots.txt`, `/favicon.ico`)
traefik.http.routers.static-router.tls: 'true'
traefik.http.routers.static-router.service: static-service
traefik.http.services.static-service.loadbalancer.healthcheck.path: /
traefik.http.services.static-service.loadbalancer.server.port: '80'
traefik:
image: traefik:1.7
command: --api --docker --defaultentrypoints=https --entryPoints='Name:http Address::80 Redirect.EntryPoint:https' --entryPoints='Name:https Address::443 TLS'
image: traefik:2.3
command:
- "--accesslog=true"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.http.address=:80"
- "--entrypoints.https.address=:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:

View File

@ -2,20 +2,25 @@
The User object has the following attributes:
- `username`: User's username.
- `email` User's email.
- `name` User's display mame.
- `is_staff` Boolean field if user is staff.
- `is_active` Boolean field if user is active.
- `date_joined` Date user joined/was created.
- `password_change_date` Date password was last changed.
- `attributes` Dynamic attributes.
- `username`: User's username.
- `email` User's email.
- `name` User's display name.
- `is_staff` Boolean field if user is staff.
- `is_active` Boolean field if user is active.
- `date_joined` Date user joined/was created.
- `password_change_date` Date password was last changed.
- `attributes` Dynamic attributes.
- `pb_groups` This is a queryset of all the user's groups.
You can do additional filtering like `user.pb_groups.filter(name__startswith='test')`, see [here](https://docs.djangoproject.com/en/3.1/ref/models/querysets/#id4)
To get the name of all groups, you can do `[group.name for group in user.pb_groups.all()]`
## Examples
List all the User's group names:
```python
for group in user.groups.all():
for group in user.pb_groups.all():
yield group.name
```

View File

@ -84,15 +84,6 @@
}
},
{
"identifiers": {
"pk": "9922212c-47a2-475a-9905-abeb5e621652"
},
"model": "passbook_policies_expression.expressionpolicy",
"attrs": {
"name": "policy-enrollment-password-equals",
"expression": "# Verifies that the passwords are equal\r\nreturn request.context['password'] == request.context['password_repeat']"
}
},{
"identifiers": {
"pk": "096e6282-6b30-4695-bd03-3b143eab5580",
"name": "default-enrollment-email-verficiation"
@ -135,9 +126,6 @@
"cb954fd4-65a5-4ad9-b1ee-180ee9559cf4",
"7db91ee8-4290-4e08-8d39-63f132402515",
"d30b5eb4-7787-4072-b1ba-65b46e928920"
],
"validation_policies": [
"9922212c-47a2-475a-9905-abeb5e621652"
]
}
},

View File

@ -55,16 +55,6 @@
"order": 1
}
},
{
"identifiers": {
"pk": "cd042fc6-cc92-4b98-b7e6-f4729df798d8"
},
"model": "passbook_policies_expression.expressionpolicy",
"attrs": {
"name": "default-password-change-password-equal",
"expression": "# Check that both passwords are equal.\nreturn request.context['password'] == request.context['password_repeat']"
}
},
{
"identifiers": {
"pk": "e54045a7-6ecb-4ad9-ad37-28e72d8e565e",
@ -111,16 +101,13 @@
{
"identifiers": {
"pk": "975d5502-1e22-4d10-b560-fbc5bd70ff4d",
"name": "default-password-change-prompt"
"name": "Change your password"
},
"model": "passbook_stages_prompt.promptstage",
"attrs": {
"fields": [
"7db91ee8-4290-4e08-8d39-63f132402515",
"d30b5eb4-7787-4072-b1ba-65b46e928920"
],
"validation_policies": [
"cd042fc6-cc92-4b98-b7e6-f4729df798d8"
]
}
},

View File

@ -39,7 +39,6 @@ This designates a flow for unenrollment. This flow can contain any amount of ver
This designates a flow for recovery. This flow normally contains an [**identification**](stages/identification/index.md) stage to find the user. It can also contain any amount of verification stages, such as [**email**](stages/email/index.md) or [**captcha**](stages/captcha/index.md).
Afterwards, use the [**prompt**](stages/prompt/index.md) stage to ask the user for a new password and the [**user_write**](stages/user_write.md) stage to update the password.
### Change Password
### Setup
This designates a flow for password changes. This flow can contain any amount of verification stages, such as [**email**](stages/email/index.md) or [**captcha**](stages/captcha/index.md).
Afterwards, use the [**prompt**](stages/prompt/index.md) stage to ask the user for a new password and the [**user_write**](stages/user_write.md) stage to update the password.
This designates a flow for general setup. This designation doesn't have any constraints in what you can do. For example, by default this designation is used to configure Factors, like change a password and setup TOTP.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 450 KiB

View File

@ -4,21 +4,28 @@ This installation method is for test-setups and small-scale productive setups.
## Prerequisites
- docker
- docker-compose
- docker
- docker-compose
## Install
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml). Place it in a directory of your choice.
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING__ENABLED=true >> .env`
To optionally deploy a different version run `echo PASSBOOK_TAG=0.12.2-stable >> .env`
If this is a fresh passbook install run the following commands to generate a password:
```
sudo apt-get install -y pwgen
echo "PG_PASS=$(pwgen 40 1)" >> .env
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
```
Afterwards, run these commands to finish
```
wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
# Optionally enable Error-reporting
# export PASSBOOK_ERROR_REPORTING=true
# Optionally deploy a different version
# export PASSBOOK_TAG=0.10.0-rc4
# If this is a productive installation, set a different PostgreSQL Password
# export PG_PASS=$(pwgen 40 1)
docker-compose pull
docker-compose up -d
docker-compose run --rm server migrate
@ -32,4 +39,6 @@ Now you can pull the Docker images needed by running `docker-compose pull`. Afte
passbook will then be reachable via HTTP on port 80, and HTTPS on port 443. You can optionally configure the packaged traefik to use Let's Encrypt certificates for TLS Encryption.
If you plan to access passbook via a reverse proxy which does SSL Termination, make sure you use the HTTPS port, so passbook is aware of the SSL connection.
The initial setup process also creates a default admin user, the username and password for which is `pbadmin`. It is highly recommended to change this password as soon as you log in.

View File

@ -4,39 +4,55 @@ For a mid to high-load installation, Kubernetes is recommended. passbook is inst
This installation automatically applies database migrations on startup. After the installation is done, you can use `pbadmin` as username and password.
```
```yaml
###################################
# Values directly affecting passbook
###################################
image:
name: beryju/passbook
name_static: beryju/passbook-static
tag: 0.9.0-stable
tag: 0.12.2-stable
nameOverride: ""
serverReplicas: 1
workerReplicas: 1
# Enable the Kubernetes integration which lets passbook deploy outposts into kubernetes
kubernetesIntegration: true
config:
# Optionally specify fixed secret_key, otherwise generated automatically
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
# secretKey: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
# Enable error reporting
error_reporting:
errorReporting:
enabled: false
environment: customer
send_pii: false
sendPii: false
# Log level used by web and worker
# Can be either debug, info, warning, error
log_level: warning
logLevel: warning
# Enable Database Backups to S3
# backup:
# access_key: access-key
# secret_key: secret-key
# accessKey: access-key
# secretKey: secret-key
# bucket: s3-bucket
# region: eu-central-1
# host: s3-host
ingress:
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
path: /
hosts:
- passbook.k8s.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - passbook.k8s.local
###################################
# Values controlling dependencies
###################################
@ -57,16 +73,4 @@ redis:
enabled: false
# https://stackoverflow.com/a/59189742
disableCommands: []
ingress:
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
path: /
hosts:
- passbook.k8s.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - passbook.k8s.local
```

View File

@ -0,0 +1,42 @@
# passbook behind a reverse-proxy
If you want to access passbook behind a reverse-proxy, use a config like this. It is important that Websocket is enabled, so that Outposts can connect.
```
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
# Server config
listen 80;
server_name sso.domain.tld;
# 301 to SSL
location / {
return 301 https://$host$request_uri;
}
}
server {
# Server config
listen 443 ssl http2;
server_name sso.domain.tld;
# SSL Certs
ssl_certificate /etc/letsencrypt/live/domain.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain.tld/privkey.pem;
# Proxy site
location / {
proxy_pass https://<hostname of your passbook server>;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
```

View File

@ -9,14 +9,14 @@
The following placeholders will be used:
- `passbook.company` is the FQDN of the passbook install.
- `passbook.company` is the FQDN of the passbook install.
Create an application in passbook and note the slug, as this will be used later. Create a SAML provider with the following parameters:
- ACS URL: `https://signin.aws.amazon.com/saml`
- Audience: `urn:amazon:webservices`
- Issuer: `passbook`
- Binding: `Post`
- ACS URL: `https://signin.aws.amazon.com/saml`
- Audience: `urn:amazon:webservices`
- Issuer: `passbook`
- Binding: `Post`
You can of course use a custom signing certificate, and adjust durations.
@ -24,10 +24,49 @@ You can of course use a custom signing certificate, and adjust durations.
Create a role with the permissions you desire, and note the ARN.
AWS requires two custom PropertyMappings; `Role` and `RoleSessionName`. Create them as following:
After you've created the Property Mappings below, add them to the Provider.
![](./property-mapping-role.png)
Create an application, assign policies, and assign this provider.
![](./property-mapping-role-session-name.png)
Export the metadata from passbook, and create an Identity Provider [here](https://console.aws.amazon.com/iam/home#/providers).
Afterwards export the metadata from passbook, and create an Identity Provider [here](https://console.aws.amazon.com/iam/home#/providers).
#### Role Mapping
The Role mapping specifies the AWS ARN(s) of the identity provider, and the role the user should assume ([see](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml_assertions.html#saml_role-attribute)).
This Mapping needs to have the SAML Name field set to "https://aws.amazon.com/SAML/Attributes/Role"
As expression, you can return a static ARN like so
```python
return "arn:aws:iam::123412341234:role/saml_role,arn:aws:iam::123412341234:saml-provider/passbook"
```
Or, if you want to assign AWS Roles based on Group membership, you can add a custom attribute to the Groups, for example "aws_role", and use this snippet below. Groups are sorted by name and later groups overwrite earlier groups' attributes.
```python
role_name = user.group_attributes().get("aws_role", "")
return f"arn:aws:iam::123412341234:role/{role_name},arn:aws:iam::123412341234:saml-provider/passbook"
```
If you want to allow a user to choose from multiple roles, use this snippet
```python
return [
"arn:aws:iam::123412341234:role/role_a,arn:aws:iam::123412341234:saml-provider/passbook",
"arn:aws:iam::123412341234:role/role_b,arn:aws:iam::123412341234:saml-provider/passbook",
"arn:aws:iam::123412341234:role/role_c,arn:aws:iam::123412341234:saml-provider/passbook",
]
```
### RoleSessionName Mapping
The RoleSessionMapping specifies what identifier will be shown at the top of the Management Console ([see](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml_assertions.html#saml_role-session-attribute)).
This mapping needs to have the SAML Name field set to "https://aws.amazon.com/SAML/Attributes/RoleSessionName".
To use the user's username, use this snippet
```python
return user.username
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@ -11,14 +11,15 @@ From https://about.gitlab.com/what-is-gitlab/
The following placeholders will be used:
- `gitlab.company` is the FQDN of the GitLab Install
- `passbook.company` is the FQDN of the passbook Install
- `gitlab.company` is the FQDN of the GitLab Install
- `passbook.company` is the FQDN of the passbook Install
Create an application in passbook and note the slug, as this will be used later. Create a SAML provider with the following parameters:
- ACS URL: `https://gitlab.company/users/auth/saml/callback`
- Audience: `https://gitlab.company`
- Issuer: `https://gitlab.company`
- ACS URL: `https://gitlab.company/users/auth/saml/callback`
- Audience: `https://gitlab.company`
- Issuer: `https://gitlab.company`
- Binding: `Post`
You can of course use a custom signing certificate, and adjust durations. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php).
@ -41,7 +42,7 @@ gitlab_rails['omniauth_providers'] = [
args: {
assertion_consumer_service_url: 'https://gitlab.company/users/auth/saml/callback',
idp_cert_fingerprint: '4E:1E:CD:67:4A:67:5A:E9:6A:D0:3C:E6:DD:7A:F2:44:2E:76:00:6A',
idp_sso_target_url: 'https://passbook.company/application/saml/<passbook application slug>/login/',
idp_sso_target_url: 'https://passbook.company/application/saml/<passbook application slug>/sso/binding/post/',
issuer: 'https://gitlab.company',
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
attribute_statements: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

View File

@ -15,27 +15,31 @@ From https://sentry.io
The following placeholders will be used:
- `sentry.company` is the FQDN of the Sentry install.
- `passbook.company` is the FQDN of the passbook install.
- `sentry.company` is the FQDN of the Sentry install.
- `passbook.company` is the FQDN of the passbook install.
Create an application in passbook. Create an OpenID provider with the following parameters:
Create an application in passbook. Create a SAML Provider with the following values
- Client Type: `Confidential`
- Response types: `code (Authorization Code Flow)`
- JWT Algorithm: `RS256`
- Redirect URIs: `https://sentry.company/auth/sso/`
- Scopes: `openid email`
- ACS URL: `https://sentry.company/saml/acs/<sentry organisation name>/`
- Audience: `https://sentry.company/saml/metadata/<sentry organisation name>/`
- Issuer: `passbook`
- Service Provider Binding: `Post`
- Property Mapping: Select all Autogenerated Mappings
## Sentry
**This guide assumes you've installed Sentry using [getsentry/onpremise](https://github.com/getsentry/onpremise)**
- Add `sentry-auth-oidc` to `onpremise/sentry/requirements.txt` (Create the file if it doesn't exist yet)
- Add the following block to your `onpremise/sentry/sentry.conf.py`:
```
OIDC_ISSUER = "passbook"
OIDC_CLIENT_ID = "<Client ID from passbook>"
OIDC_CLIENT_SECRET = "<Client Secret from passbook>"
OIDC_SCOPE = "openid email"
OIDC_DOMAIN = "https://passbook.company/application/oidc/"
```
Navigate to Settings -> Auth, and click on Configure next to SAML2
![](./auth.png)
In passbook, get the Metadata URL by right-clicking `Download Metadata` and selecting Copy Link Address, and paste that URL into Sentry.
On the next screen, input these Values
IdP User ID: `urn:oid:0.9.2342.19200300.100.1.1`
User Email: `urn:oid:0.9.2342.19200300.100.1.3`
First Name: `urn:oid:2.5.4.3`
After confirming, Sentry will authenticate with passbook, and you should be redirected back to a page confirming your settings.

View File

@ -0,0 +1,37 @@
# Sonarr Integration
!!! note
These instructions apply to all projects in the *arr Family. If you use multiple of these projects, you can assign them to the same Outpost.
## What is Sonarr
From https://github.com/Sonarr/Sonarr
!!! note ""
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
## Preparation
The following placeholders will be used:
- `sonarr.company` is the FQDN of the Sonarr install.
- `passbook.company` is the FQDN of the passbook install.
Create an application in passbook. Create a Proxy Provider with the following values
- Internal host
If Sonarr is running in docker, and you're deploying the passbook proxy on the same host, set the value to `http://sonarr:8989`, where sonarr is the name of your container.
If Sonarr is running on a different server than where you are deploying the passbook proxy, set the value to `http://sonarr.company:8989`.
- External host
Set this to the external URL you will be accessing Sonarr from.
## Deployment
Create an outpost deployment for the provider you've created above, as described [here](../../../outposts/outposts.md). Deploy this Outpost either on the same host or a different host that can access Sonarr.
The outpost will connect to passbook and configure itself.

View File

@ -0,0 +1,50 @@
# Tautulli Integration
## What is Tautulli
From https://tautulli.com/
!!! note
Tautulli is a 3rd party application that you can run alongside your Plex Media Server to monitor activity and track various statistics. Most importantly, these statistics include what has been watched, who watched it, when and where they watched it, and how it was watched. The only thing missing is "why they watched it", but who am I to question your 42 plays of Frozen. All statistics are presented in a nice and clean interface with many tables and graphs, which makes it easy to brag about your server to everyone else.
## Preparation
The following placeholders will be used:
- `tautulli.company` is the FQDN of the Tautulli install.
- `passbook.company` is the FQDN of the passbook install.
## passbook Setup
Because Tautulli requires valid HTTP Basic credentials, you must save your HTTP Basic Credentials in passbook. The recommended way to do this is to create a Group. Name the group "Tautulli Users", for example. For this group, add the following attributes:
```yaml
tautulli_user: username
tautulli_password: password
```
Add all Tautulli users to the Group. You should also create a Group Membership Policy to limit access to the application.
Create an application in passbook. Create a Proxy provider with the following parameters:
- Internal host
If Tautulli is running in docker, and you're deploying the passbook proxy on the same host, set the value to `http://tautulli:3579`, where tautulli is the name of your container.
If Tautulli is running on a different server to where you are deploying the passbook proxy, set the value to `http://tautulli.company:3579`.
- External host
Set this to the external URL you will be accessing Tautulli from.
Enable the `Set HTTP-Basic Authentication` option. Set and `HTTP-Basic Username` and `HTTP-Basic Password` to `tautulli_user` and `tautulli_password` respectively. These values can be chosen freely, `tautulli_` is just used as a prefix for clarity.
## Tautulli Setup
In Tautulli, navigate to Settings and enable the "Show Advanced" option. Navigate to "Web Interface" on the sidebar, and ensure the Option `Use Basic Authentication` is checked.
![](./tautulli.png)
Save the settings, and restart Tautulli if prompted.
Afterwards, you need to deploy an Outpost in front of Tautulli, as descried [here](../sonarr/index.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -16,14 +16,15 @@ From https://docs.ansible.com/ansible/2.5/reference_appendices/tower.html
The following placeholders will be used:
- `awx.company` is the FQDN of the AWX/Tower install.
- `passbook.company` is the FQDN of the passbook install.
- `awx.company` is the FQDN of the AWX/Tower install.
- `passbook.company` is the FQDN of the passbook install.
Create an application in passbook and note the slug, as this will be used later. Create a SAML provider with the following parameters:
- ACS URL: `https://awx.company/sso/complete/saml/`
- Audience: `awx`
- Issuer: `https://awx.company/sso/metadata/saml/`
- ACS URL: `https://awx.company/sso/complete/saml/`
- Audience: `awx`
- Service Provider Binding: Post
- Issuer: `https://awx.company/sso/metadata/saml/`
You can of course use a custom signing certificate, and adjust durations.

View File

@ -0,0 +1,58 @@
# Ubuntu Landscape Integration
## What is Ubuntu Landscape
From https://en.wikipedia.org/wiki/Landscape_(software)
!!! note ""
Landscape is a systems management tool developed by Canonical. It can be run on-premises or in the cloud depending on the needs of the user. It is primarily designed for use with Ubuntu derivatives such as Desktop, Server, and Core.
!!! warning
This requires passbook 0.10.3 or newer.
## Preparation
The following placeholders will be used:
- `landscape.company` is the FQDN of the Landscape server.
- `passbook.company` is the FQDN of the passbook install.
Landscape uses the OpenID-Connect Protocol for single-sign on.
## passbook Setup
Create an OAuth2/OpenID-Connect Provider with the default settings. Set the Redirect URIs to `https://landscape.company/login/handle-openid`. Select all Autogenerated Scopes.
Keep Note of the Client ID and the Client Secret.
Create an application and assign access policies to the application. Set the application's provider to the provider you've just created.
## Landscape Setup
On the Landscape Server, edit the file `/etc/landscape/service.conf` and add the following snippet under the `[landscape]` section:
```
oidc-issuer = https://passbook.company/application/o/<slug of the application you've created>/
oidc-client-id = <client ID of the provider you've created>
oidc-client-secret = <client Secret of the provider you've created>
```
Afterwards, run `sudo lsctl restart` to restart the Landscape services.
## Appendix
To make an OpenID-Connect User admin, you have to insert some rows into the database.
First login with your passbook user, and make sure the user is created successfully.
Run `sudo -u postgres psql landscape-standalone-main` on the Landscape server to open a PostgreSQL Prompt.
Then run `select * from person;` to get a list of all users. Take note of the ID given to your new user.
Run the following commands to make this user an administrator:
```sql
INSERT INTO person_account VALUES (<user id>, 1);
INSERT INTO person_access VALUES (<user id>, 1, 1);
```

View File

@ -0,0 +1,83 @@
# VMware vCenter Integration
## What is vCenter
From https://en.wikipedia.org/wiki/VCenter
!!! note ""
vCenter Server is the centralized management utility for VMware, and is used to manage virtual machines, multiple ESXi hosts, and all dependent components from a single centralized location. VMware vMotion and svMotion require the use of vCenter and ESXi hosts.
!!! warning
This requires passbook 0.10.3 or newer.
!!! warning
This requires VMware vCenter 7.0.0 or newer.
!!! note
It seems that the vCenter still needs to be joined to the Active Directory Domain, otherwise group membership does not work correctly. We're working on a fix for this, for the meantime your vCenter should be part of your Domain.
## Preparation
The following placeholders will be used:
- `vcenter.company` is the FQDN of the vCenter server.
- `passbook.company` is the FQDN of the passbook install.
Since vCenter only allows OpenID-Connect in combination with Active Directory, it is recommended to have passbook sync with the same Active Directory.
### Step 1
Under *Property Mappings*, create a *Scope Mapping*. Give it a name like "OIDC-Scope-VMware-vCenter". Set the scope name to `openid` and the expression to the following
```python
return {
"domain": "<your active directory domain>",
}
```
### Step 2
!!! note
If your Active Directory Schema is the same as your Email address schema, skip to Step 3.
Under *Sources*, click *Edit* and ensure that "Autogenerated Active Directory Mapping: userPrincipalName -> attributes.upn" has been added to your source.
### Step 3
Under *Providers*, create an OAuth2/OpenID Provider with these settings:
- Client Type: Confidential
- Response Type: code (ADFS Compatibility Mode, sends id_token as access_token)
- JWT Algorithm: RS256
- Redirect URI: `https://vcenter.company/ui/login/oauth2/authcode`
- Post Logout Redirect URIs: `https://vcenter.company/ui/login`
- Sub Mode: If your Email address Schema matches your UPN, select "Based on the User's Email...", otherwise select "Based on the User's UPN...".
- Scopes: Select the Scope Mapping you've created in Step 1
![](./passbook_setup.png)
### Step 4
Create an application which uses this provider. Optionally apply access restrictions to the application.
Set the Launch URL to `https://vcenter.company/ui/login/oauth2`. This will skip vCenter's User Prompt and directly log you in.
## vCenter Setup
Login as local Administrator account (most likely ends with vsphere.local). Using the Menu in the Navigation bar, navigate to *Administration -> Single Sing-on -> Configuration*.
Click on *Change Identity Provider* in the top-right corner.
In the wizard, select "Microsoft ADFS" and click Next.
Fill in the Client Identifier and Shared Secret from the Provider in passbook. For the OpenID Address, click on *View Setup URLs* in passbook, and copy the OpenID Configuration URL.
On the next page, fill in your Active Directory Connection Details. These should be similar to what you have set in passbook.
![](./vcenter_post_setup.png)
If your vCenter was already setup with LDAP beforehand, your Role assignments will continue to work.

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -0,0 +1,55 @@
# Active Directory Integration
## Preparation
The following placeholders will be used:
- `ad.company` is the Name of the Active Directory domain.
- `passbook.company` is the FQDN of the passbook install.
## Active Directory Setup
1. Open Active Directory Users and Computers
2. Create a user in Active Directory, matching your naming scheme
![](./01_user_create.png)
3. Give the User a password, generated using for example `pwgen 64 1`.
4. Open the Delegation of Control Wizard by right-clicking the domain.
5. Select the passbook service user you've just created.
6. Ensure the "Reset user password and force password change at next logon" Option is checked.
![](./02_delegate.png)
## passbook Setup
In passbook, create a new LDAP Source in Administration -> Sources.
Use these settings:
- Server URI: `ldap://ad.company`
For passbook to be able to write passwords back to Active Directory, make sure to use `ldaps://`
- Bind CN: `<name of your service user>@ad.company`
- Bind Password: The password you've given the user above
- Base DN: The base DN which you want passbook to sync
- Property Mappings: Select all and click the right arrow
The other settings might need to be adjusted based on the setup of your domain.
- Addition User/Group DN: Additional DN which is *prepended* to your Base DN for user synchronization.
- Addition Group DN: Additional DN which is *prepended* to your Base DN for group synchronization.
- User object filter: Which objects should be considered users.
- Group object filter: Which objects should be considered groups.
- User group membership field: Which user field saves the group membership
- Object uniqueness field: A user field which contains a unique Identifier
- Sync parent group: If enabled, all synchronized groups will be given this group as a parent.
After you save the source, a synchronization will start in the background. When its done, you cen see the summary on the System Tasks page.
![](./03_pb_status.png)

View File

@ -0,0 +1,111 @@
# Backup and restore
!!! warning
Local backups are only supported for docker-compose installs. If you want to backup a Kubernetes instance locally, use an S3-compatible server such as [minio](https://min.io/)
### Backup
Local backups can be created by running the following command in your passbook installation directory
```
docker-compose run --rm worker backup
```
This will dump the current database into the `./backups` folder. By defaults, the last 10 Backups are kept.
To schedule these backups, use the following snippet in a crontab
```
0 0 * * * bash -c "cd <passbook install location> && docker-compose run --rm worker backup" >/dev/null
```
!!! notice
passbook does support automatic backups on a schedule, however this is currently not recommended, as there is no way to monitor these scheduled tasks.
### Restore
Run this command in your passbook installation directory
```
docker-compose run --rm worker restore
```
This will prompt you to restore from your last backup. If you want to restore from a specific file, use the `-i` flag with the filename:
```
docker-compose run --rm worker restore -i default-2020-10-03-115557.psql
```
After you've restored the backup, it is recommended to restart all services with `docker-compose restart`.
### S3 Configuration
!!! notice
To trigger backups with S3 enabled, use the same commands as above.
#### S3 Preparation
passbook expects the bucket you select to already exist. The IAM User given to passbook should have the following permissions
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObjectAcl",
"s3:GetObject",
"s3:ListBucket",
"s3:DeleteObject",
"s3:PutObjectAcl"
],
"Principal": {
"AWS": "arn:aws:iam::example-AWS-account-ID:user/example-user-name"
},
"Resource": [
"arn:aws:s3:::example-bucket-name/*",
"arn:aws:s3:::example-bucket-name"
]
}
]
}
```
#### docker-compose
Set the following values in your `.env` file.
```
PASSBOOK_POSTGRESQL__S3_BACKUP__ACCESS_KEY=
PASSBOOK_POSTGRESQL__S3_BACKUP__SECRET_KEY=
PASSBOOK_POSTGRESQL__S3_BACKUP__BUCKET=
PASSBOOK_POSTGRESQL__S3_BACKUP__REGION=
```
If you want to backup to an S3-compatible server, like [minio](https://min.io/), use this setting:
```
PASSBOOK_POSTGRESQL__S3_BACKUP__HOST=http://play.min.io
```
#### Kubernetes
Simply enable these options in your values.yaml file
```yaml
# Enable Database Backups to S3
backup:
access_key: access-key
secret_key: secret-key
bucket: s3-bucket
region: eu-central-1
host: s3-host
```
Afterwards, run a `helm upgrade` to update the ConfigMap. Because passbook-scheduled backups are not recommended currently, a Kubernetes CronJob is created that runs the backup daily.

View File

@ -0,0 +1,20 @@
# Outpost deployment in docker-compose
To deploy an outpost with docker-compose, use this snippet in your docker-compose file.
You can also run the outpost in a separate docker-compose project, you just have to ensure that the outpost container can reach your application container.
```yaml
version: '3.5'
services:
passbook_proxy:
image: beryju/passbook-proxy:0.10.0-stable
ports:
- 4180:4180
- 4443:4443
environment:
PASSBOOK_HOST: https://your-passbook.tld
PASSBOOK_INSECURE: 'false'
PASSBOOK_TOKEN: token-generated-by-passbook
```

View File

@ -0,0 +1,99 @@
# Outpost deployment on Kubernetes
Use the following manifest, replacing all values surrounded with `__`.
Afterwards, configure the proxy provider to connect to `<service name>.<namespace>.svc.cluster.local`, and update your Ingress to connect to the `passbook-outpost` service.
```yaml
apiVersion: v1
kind: Secret
metadata:
labels:
app.kubernetes.io/instance: test
app.kubernetes.io/managed-by: passbook.beryju.org
app.kubernetes.io/name: passbook-proxy
app.kubernetes.io/version: 0.10.0
name: passbook-outpost-api
stringData:
passbook_host: '__PASSBOOK_URL__'
passbook_host_insecure: 'true'
token: '__PASSBOOK_TOKEN__'
type: Opaque
---
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/instance: test
app.kubernetes.io/managed-by: passbook.beryju.org
app.kubernetes.io/name: passbook-proxy
app.kubernetes.io/version: 0.10.0
name: passbook-outpost
spec:
ports:
- name: http
port: 4180
protocol: TCP
targetPort: http
- name: https
port: 4443
protocol: TCP
targetPort: https
selector:
app.kubernetes.io/instance: test
app.kubernetes.io/managed-by: passbook.beryju.org
app.kubernetes.io/name: passbook-proxy
app.kubernetes.io/version: 0.10.0
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/instance: test
app.kubernetes.io/managed-by: passbook.beryju.org
app.kubernetes.io/name: passbook-proxy
app.kubernetes.io/version: 0.10.0
name: passbook-outpost
spec:
selector:
matchLabels:
app.kubernetes.io/instance: test
app.kubernetes.io/managed-by: passbook.beryju.org
app.kubernetes.io/name: passbook-proxy
app.kubernetes.io/version: 0.10.0
template:
metadata:
labels:
app.kubernetes.io/instance: test
app.kubernetes.io/managed-by: passbook.beryju.org
app.kubernetes.io/name: passbook-proxy
app.kubernetes.io/version: 0.10.0
spec:
containers:
- env:
- name: PASSBOOK_HOST
valueFrom:
secretKeyRef:
key: passbook_host
name: passbook-outpost-api
- name: PASSBOOK_TOKEN
valueFrom:
secretKeyRef:
key: token
name: passbook-outpost-api
- name: PASSBOOK_INSECURE
valueFrom:
secretKeyRef:
key: passbook_host_insecure
name: passbook-outpost-api
image: beryju/passbook-proxy:0.10.0-stable
name: proxy
ports:
- containerPort: 4180
name: http
protocol: TCP
- containerPort: 4443
name: https
protocol: TCP
```

View File

@ -6,21 +6,9 @@ An outpost is a single deployment of a passbook component, which can be deployed
Upon creation, a service account and a token is generated. The service account only has permissions to read the outpost and provider configuration. This token is used by the Outpost to connect to passbook.
To deploy an outpost, you can for example use this docker-compose snippet:
To deploy an outpost, see: <a name="deploy">
```yaml
version: 3.5
- [Kubernetes](deploy-kubernetes.md)
- [docker-compose](deploy-docker-compose.md)
services:
passbook_proxy:
image: beryju/passbook-proxy:0.10.0-stable
ports:
- 4180:4180
- 4443:4443
environment:
PASSBOOK_HOST: https://your-passbook.tld
PASSBOOK_INSECURE: 'true'
PASSBOOK_TOKEN: token-generated-by-passbook
```
In future versions, this snippet will be automatically generated. You will also be able to deploy an outpost directly into a kubernetes cluster.w
In future versions, this snippet will be automatically generated. You will also be able to deploy an outpost directly into a kubernetes cluster.

View File

@ -0,0 +1,9 @@
# Upgrading an Outpost
In the Outpost Overview list, you'll see if any deployed outposts are out of date.
![](./upgrading_outdated.png)
To upgrade the Outpost to the latest version, simple adjust the docker tag of the outpost the the new version.
Since the configuration is managed by passbook, that's all you have to do.

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -27,4 +27,11 @@ return False
- `request.context`: A dictionary with dynamic data. This depends on the origin of the execution.
- `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external provider.
- `pb_client_ip`: Client's IP Address or '255.255.255.255' if no IP Address could be extracted. Can be [compared](../expressions/index.md#comparing-ip-addresses)
- `pb_flow_plan`: Current Plan if Policy is called from the Flow Planner.
Additionally, when the policy is executed from a flow, every variable from the flow's current context is accessible under the `context` object.
This includes the following:
- `prompt_data`: Data which has been saved from a prompt stage or an external source.
- `application`: The application the user is in the process of authorizing.
- `pending_user`: The currently pending user

View File

@ -0,0 +1,11 @@
# Troubleshooting access problems
## I get an access denied error when trying to access an application.
If your user is a superuser, or has the attribute `passbook_user_debug` set to true:
![](./passbook_user_debug.png)
Afterwards, try to access the application again. You will now see a message explaining which policy denied you access:
![](./access_denied_message.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

20
docs/upgrading/to-0.11.md Normal file
View File

@ -0,0 +1,20 @@
# Upgrading to 0.11
This update brings these headline features:
- Add Backup and Restore, currently only externally schedulable, documented [here](https://passbook.beryju.org/maintenance/backups/)
- New Admin Dashboard with more metrics and Charts
Shows successful and failed logins from the last 24 hours, as well as the most used applications
- Add search to all table views
- Outpost now supports a Docker Controller, which installs the Outpost on the same host as passbook, updates and manages it
- Add Token Identifier
Tokens now have an identifier which is used to reference to them, so the Primary key is not shown in URLs
- `core/applications/list` API now shows applications the user has access to via policies
## Upgrading
This upgrade can be done as with minor upgrades, the only external change is the new docker-compose file, which enabled the Docker Integration for Outposts. To use this feature, please download the latest docker-compose from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml).
Afterwards, you can simply run `docker-compose up -d` and then the normal upgrade command of `docker-compose run --rm server migrate`.

63
docs/upgrading/to-0.12.md Normal file
View File

@ -0,0 +1,63 @@
# Upgrading to 0.12
This update brings these headline features:
- Rewrite Outpost state Logic, which now supports multiple concurrent Outpost instances.
- Add Kubernetes Integration for Outposts, which deploys and maintains Outposts with High Availability in a Kubernetes Cluster
- Add System Task Overview to see all background tasks, their status, the log output, and retry them
- Alerts now disappear automatically
- Audit Logs are now searchable
- Users can now create their own Tokens to access the API
- docker-compose deployment now uses traefik 2.3
Fixes:
- Fix high CPU Usage of the proxy when Websocket connections fail
## Upgrading
### docker-compose
Docker-compose users should download the latest docker-compose file from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml). This includes the new traefik 2.3.
Afterwards, you can simply run `docker-compose up -d` and then the normal upgrade command of `docker-compose run --rm server migrate`.
### Kubernetes
For Kubernetes users, there are some changes to the helm values.
The values change from
```yaml
config:
# Optionally specify fixed secret_key, otherwise generated automatically
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
# Enable error reporting
error_reporting:
enabled: false
environment: customer
send_pii: false
# Log level used by web and worker
# Can be either debug, info, warning, error
log_level: warning
```
to
```yaml
config:
# Optionally specify fixed secret_key, otherwise generated automatically
# secretKey: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
# Enable error reporting
errorReporting:
enabled: false
environment: customer
sendPii: false
# Log level used by web and worker
# Can be either debug, info, warning, error
logLevel: warning
```
in order to be consistent with the rest of the settings.
There is also a new setting called `kubernetesIntegration`, which controls the Kubernetes integration for passbook. When enabled (the default), a Service Account is created, which allows passbook to deploy and update Outposts.

View File

@ -2,7 +2,7 @@ version: '3.7'
services:
chrome:
image: selenium/standalone-chrome:3.141.59-20200525
image: selenium/standalone-chrome:3.141
volumes:
- /dev/shm:/dev/shm
network_mode: host

View File

@ -2,7 +2,7 @@ version: '3.7'
services:
chrome:
image: selenium/standalone-chrome-debug:3.141.59-20200525
image: selenium/standalone-chrome-debug:3.141
volumes:
- /dev/shm:/dev/shm
network_mode: host

View File

@ -1,58 +1,38 @@
"""Test Enroll flow"""
from time import sleep
from sys import platform
from typing import Any, Dict, Optional
from unittest.case import skipUnless
from django.test import override_settings
from docker import DockerClient, from_env
from docker.models.containers import Container
from docker.types import Healthcheck
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec
from structlog import get_logger
from e2e.utils import USER, SeleniumTestCase
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.policies.expression.models import ExpressionPolicy
from passbook.stages.email.models import EmailStage, EmailTemplates
from passbook.stages.identification.models import IdentificationStage
from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage
from passbook.stages.user_login.models import UserLoginStage
from passbook.stages.user_write.models import UserWriteStage
LOGGER = get_logger()
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestFlowsEnroll(SeleniumTestCase):
"""Test Enroll flow"""
def setUp(self):
self.container = self.setup_client()
super().setUp()
def setup_client(self) -> Container:
"""Setup test IdP container"""
client: DockerClient = from_env()
container = client.containers.run(
image="mailhog/mailhog:v1.0.1",
detach=True,
network_mode="host",
auto_remove=True,
healthcheck=Healthcheck(
def get_container_specs(self) -> Optional[Dict[str, Any]]:
return {
"image": "mailhog/mailhog:v1.0.1",
"detach": True,
"network_mode": "host",
"auto_remove": True,
"healthcheck": Healthcheck(
test=["CMD", "wget", "--spider", "http://localhost:8025"],
interval=5 * 100 * 1000000,
start_period=1 * 100 * 1000000,
),
)
while True:
container.reload()
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
if status == "healthy":
return container
LOGGER.info("Container failed healthcheck")
sleep(1)
def tearDown(self):
self.container.kill()
super().tearDown()
}
def test_enroll_2_step(self):
"""Test 2-step enroll flow"""
@ -78,16 +58,9 @@ class TestFlowsEnroll(SeleniumTestCase):
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
)
# Password checking policy
password_policy = ExpressionPolicy.objects.create(
name="policy-enrollment-password-equals",
expression="return request.context['password'] == request.context['password_repeat']",
)
# Stages
first_stage = PromptStage.objects.create(name="prompt-stage-first")
first_stage.fields.set([username_prompt, password, password_repeat])
first_stage.validation_policies.set([password_policy])
first_stage.save()
second_stage = PromptStage.objects.create(name="prompt-stage-second")
second_stage.fields.set([name_field, email])
@ -131,7 +104,7 @@ class TestFlowsEnroll(SeleniumTestCase):
self.wait_for_url(self.url("passbook_core:user-settings"))
self.assertEqual(
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
self.driver.find_element(By.ID, "user-settings").text,
"foo",
)
self.assertEqual(
@ -171,16 +144,9 @@ class TestFlowsEnroll(SeleniumTestCase):
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
)
# Password checking policy
password_policy = ExpressionPolicy.objects.create(
name="policy-enrollment-password-equals",
expression="return request.context['password'] == request.context['password_repeat']",
)
# Stages
first_stage = PromptStage.objects.create(name="prompt-stage-first")
first_stage.fields.set([username_prompt, password, password_repeat])
first_stage.validation_policies.set([password_policy])
first_stage.save()
second_stage = PromptStage.objects.create(name="prompt-stage-second")
second_stage.fields.set([name_field, email])
@ -220,30 +186,30 @@ class TestFlowsEnroll(SeleniumTestCase):
self.driver.find_element(By.ID, "id_name").send_keys("some name")
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
sleep(3)
# Wait for the success message so we know the email is sent
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, ".pf-c-form > p"))
)
# Open Mailhog
self.driver.get("http://localhost:8025")
# Click on first message
self.wait.until(
ec.presence_of_element_located((By.CLASS_NAME, "msglist-message"))
)
self.driver.find_element(By.CLASS_NAME, "msglist-message").click()
sleep(3)
self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane"))
self.driver.find_element(By.ID, "confirm").click()
self.driver.close()
self.driver.switch_to.window(self.driver.window_handles[0])
# We're now logged in
sleep(3)
self.wait.until(
ec.presence_of_element_located(
(By.XPATH, "//a[contains(@href, '/-/user/')]")
)
)
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
self.driver.find_element(By.ID, "user-settings").click()
self.assertEqual(
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
self.driver.find_element(By.ID, "user-settings").text,
"foo",
)
self.assertEqual(

View File

@ -1,10 +1,14 @@
"""test default login flow"""
from sys import platform
from unittest.case import skipUnless
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from e2e.utils import USER, SeleniumTestCase
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestFlowsLogin(SeleniumTestCase):
"""test default login flow"""
@ -17,6 +21,6 @@ class TestFlowsLogin(SeleniumTestCase):
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.assertEqual(
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
self.driver.find_element(By.ID, "user-settings").text,
USER().username,
)

141
e2e/test_flows_otp.py Normal file
View File

@ -0,0 +1,141 @@
"""test flow with otp stages"""
from base64 import b32decode
from sys import platform
from time import sleep
from unittest.case import skipUnless
from urllib.parse import parse_qs, urlparse
from django_otp.oath import TOTP
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
from django_otp.plugins.otp_totp.models import TOTPDevice
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
from e2e.utils import USER, SeleniumTestCase
from passbook.flows.models import Flow, FlowStageBinding
from passbook.stages.otp_validate.models import OTPValidateStage
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestFlowsOTP(SeleniumTestCase):
"""test flow with otp stages"""
def test_otp_validate(self):
"""test flow with otp stages"""
sleep(1)
# Setup TOTP Device
user = USER()
device = TOTPDevice.objects.create(user=user, confirmed=True, digits=6)
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
# Move the user_login stage to order 3
FlowStageBinding.objects.filter(target=flow, order=2).update(order=3)
FlowStageBinding.objects.create(
target=flow, order=2, stage=OTPValidateStage.objects.create()
)
self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
# Get expected token
totp = TOTP(device.bin_key, device.step, device.t0, device.digits, device.drift)
self.driver.find_element(By.ID, "id_code").send_keys(totp.token())
self.driver.find_element(By.ID, "id_code").send_keys(Keys.ENTER)
self.wait_for_url(self.url("passbook_core:overview"))
self.assertEqual(
self.driver.find_element(By.ID, "user-settings").text,
USER().username,
)
def test_otp_totp_setup(self):
"""test TOTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.assertEqual(
self.driver.find_element(By.ID, "user-settings").text,
USER().username,
)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
self.driver.get(self.url("passbook_core:user-settings"))
self.driver.find_element(By.LINK_TEXT, "Time-based OTP").click()
# Remember the current URL as we should end up back here
destination_url = self.driver.current_url
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-card__body a.pf-c-button"
).click()
self.wait.until(ec.presence_of_element_located((By.ID, "qr")))
otp_uri = self.driver.find_element(By.ID, "qr").get_attribute("data-otpuri")
# Parse the OTP URI, extract the secret and get the next token
otp_args = urlparse(otp_uri)
self.assertEqual(otp_args.scheme, "otpauth")
otp_qs = parse_qs(otp_args.query)
secret_key = b32decode(otp_qs["secret"][0])
totp = TOTP(secret_key)
self.driver.find_element(By.ID, "id_code").send_keys(totp.token())
self.driver.find_element(By.ID, "id_code").send_keys(Keys.ENTER)
self.wait_for_url(destination_url)
sleep(1)
self.assertTrue(TOTPDevice.objects.filter(user=USER(), confirmed=True).exists())
def test_otp_static_setup(self):
"""test Static OTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.assertEqual(
self.driver.find_element(By.ID, "user-settings").text,
USER().username,
)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
self.driver.find_element(By.ID, "user-settings").click()
self.wait_for_url(self.url("passbook_core:user-settings"))
self.driver.find_element(By.LINK_TEXT, "Static OTP").click()
# Remember the current URL as we should end up back here
destination_url = self.driver.current_url
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-card__body a.pf-c-button"
).click()
token = self.driver.find_element(
By.CSS_SELECTOR, ".pb-otp-tokens li:nth-child(1)"
).text
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
self.wait_for_url(destination_url)
sleep(1)
self.assertTrue(
StaticDevice.objects.filter(user=USER(), confirmed=True).exists()
)
device = StaticDevice.objects.filter(user=USER(), confirmed=True).first()
self.assertTrue(StaticToken.objects.filter(token=token, device=device).exists())

View File

@ -1,7 +1,6 @@
"""test stage setup flows (password change)"""
import string
from random import SystemRandom
from time import sleep
from sys import platform
from unittest.case import skipUnless
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
@ -9,9 +8,11 @@ from selenium.webdriver.common.keys import Keys
from e2e.utils import USER, SeleniumTestCase
from passbook.core.models import User
from passbook.flows.models import Flow, FlowDesignation
from passbook.providers.oauth2.generators import generate_client_secret
from passbook.stages.password.models import PasswordStage
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestFlowsStageSetup(SeleniumTestCase):
"""test stage setup flows"""
@ -19,18 +20,15 @@ class TestFlowsStageSetup(SeleniumTestCase):
"""test password change flow"""
# Ensure that password stage has change_flow set
flow = Flow.objects.get(
slug="default-password-change", designation=FlowDesignation.STAGE_SETUP,
slug="default-password-change",
designation=FlowDesignation.STAGE_CONFIGURATION,
)
stages = PasswordStage.objects.filter(name="default-authentication-password")
stage = stages.first()
stage.change_flow = flow
stage = PasswordStage.objects.get(name="default-authentication-password")
stage.configure_flow = flow
stage.save()
new_password = "".join(
SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8)
)
new_password = generate_client_secret()
self.driver.get(
f"{self.live_server_url}/flows/default-authentication-flow/?next=%2F"
@ -40,7 +38,7 @@ class TestFlowsStageSetup(SeleniumTestCase):
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
self.driver.find_element(By.ID, "user-settings").click()
self.wait_for_url(self.url("passbook_core:user-settings"))
self.driver.find_element(By.LINK_TEXT, "Change password").click()
self.driver.find_element(By.ID, "id_password").send_keys(new_password)
@ -48,7 +46,7 @@ class TestFlowsStageSetup(SeleniumTestCase):
self.driver.find_element(By.ID, "id_password_repeat").send_keys(new_password)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
sleep(2)
self.wait_for_url(self.url("passbook_core:user-settings"))
# Because USER() is cached, we need to get the user manually here
user = User.objects.get(username=USER().username)
self.assertTrue(user.check_password(new_password))

View File

@ -1,12 +1,13 @@
"""test OAuth Provider flow"""
from sys import platform
from time import sleep
from typing import Any, Dict, Optional
from unittest.case import skipUnless
from docker import DockerClient, from_env
from docker.models.containers import Container
from docker.types import Healthcheck
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from structlog import get_logger
from selenium.webdriver.support import expected_conditions as ec
from e2e.utils import USER, SeleniumTestCase
from passbook.core.models import Application
@ -19,32 +20,29 @@ from passbook.providers.oauth2.generators import (
)
from passbook.providers.oauth2.models import ClientTypes, OAuth2Provider, ResponseTypes
LOGGER = get_logger()
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestProviderOAuth2Github(SeleniumTestCase):
"""test OAuth Provider flow"""
def setUp(self):
self.client_id = generate_client_id()
self.client_secret = generate_client_secret()
self.container = self.setup_client()
super().setUp()
def setup_client(self) -> Container:
def get_container_specs(self) -> Optional[Dict[str, Any]]:
"""Setup client grafana container which we test OAuth against"""
client: DockerClient = from_env()
container = client.containers.run(
image="grafana/grafana:7.1.0",
detach=True,
network_mode="host",
auto_remove=True,
healthcheck=Healthcheck(
return {
"image": "grafana/grafana:7.1.0",
"detach": True,
"network_mode": "host",
"auto_remove": True,
"healthcheck": Healthcheck(
test=["CMD", "wget", "--spider", "http://localhost:3000"],
interval=5 * 100 * 1000000,
start_period=1 * 100 * 1000000,
),
environment={
"environment": {
"GF_AUTH_GITHUB_ENABLED": "true",
"GF_AUTH_GITHUB_ALLOW_SIGN_UP": "true",
"GF_AUTH_GITHUB_CLIENT_ID": self.client_id,
@ -61,22 +59,10 @@ class TestProviderOAuth2Github(SeleniumTestCase):
),
"GF_LOG_LEVEL": "debug",
},
)
while True:
container.reload()
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
if status == "healthy":
return container
LOGGER.info("Container failed healthcheck")
sleep(1)
def tearDown(self):
self.container.kill()
super().tearDown()
}
def test_authorization_consent_implied(self):
"""test OAuth Provider flow (default authorization flow with implied consent)"""
sleep(1)
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
@ -91,7 +77,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
authorization_flow=authorization_flow,
)
Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
name="Grafana",
slug="grafana",
provider=provider,
)
self.driver.get("http://localhost:3000")
@ -103,7 +91,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url("http://localhost:3000/?orgId=1")
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
self.driver.get("http://localhost:3000/profile")
self.assertEqual(
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
USER().username,
@ -129,7 +117,6 @@ class TestProviderOAuth2Github(SeleniumTestCase):
def test_authorization_consent_explicit(self):
"""test OAuth Provider flow (default authorization flow with explicit consent)"""
sleep(1)
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-explicit-consent"
@ -144,7 +131,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
authorization_flow=authorization_flow,
)
app = Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
name="Grafana",
slug="grafana",
provider=provider,
)
self.driver.get("http://localhost:3000")
@ -155,23 +144,23 @@ class TestProviderOAuth2Github(SeleniumTestCase):
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.assertIn(
sleep(1)
self.assertEqual(
app.name,
self.driver.find_element(
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
).text,
self.driver.find_element(By.ID, "application-name").text,
)
self.assertEqual(
"GitHub Compatibility: Access you Email addresses",
self.driver.find_element(
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]"
).text,
self.driver.find_element(By.ID, "scope-user:email").text,
)
sleep(1)
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
self.driver.find_element(
By.CSS_SELECTOR,
("[type=submit]"),
).click()
self.wait_for_url("http://localhost:3000/?orgId=1")
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
self.driver.get("http://localhost:3000/profile")
self.assertEqual(
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
USER().username,
@ -197,7 +186,6 @@ class TestProviderOAuth2Github(SeleniumTestCase):
def test_denied(self):
"""test OAuth Provider flow (default authorization flow, denied)"""
sleep(1)
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-explicit-consent"
@ -212,7 +200,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
authorization_flow=authorization_flow,
)
app = Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
name="Grafana",
slug="grafana",
provider=provider,
)
negative_policy = ExpressionPolicy.objects.create(
@ -227,7 +217,10 @@ class TestProviderOAuth2Github(SeleniumTestCase):
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url(self.url("passbook_flows:denied"))
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
"Permission denied",

View File

@ -0,0 +1,375 @@
"""test OAuth2 OpenID Provider flow"""
from sys import platform
from time import sleep
from typing import Any, Dict, Optional
from unittest.case import skipUnless
from docker.types import Healthcheck
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
from structlog import get_logger
from e2e.utils import USER, SeleniumTestCase
from passbook.core.models import Application
from passbook.crypto.models import CertificateKeyPair
from passbook.flows.models import Flow
from passbook.policies.expression.models import ExpressionPolicy
from passbook.policies.models import PolicyBinding
from passbook.providers.oauth2.constants import (
SCOPE_OPENID,
SCOPE_OPENID_EMAIL,
SCOPE_OPENID_PROFILE,
)
from passbook.providers.oauth2.generators import (
generate_client_id,
generate_client_secret,
)
from passbook.providers.oauth2.models import (
ClientTypes,
OAuth2Provider,
ResponseTypes,
ScopeMapping,
)
LOGGER = get_logger()
APPLICATION_SLUG = "grafana"
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestProviderOAuth2OAuth(SeleniumTestCase):
"""test OAuth with OAuth Provider flow"""
def setUp(self):
self.client_id = generate_client_id()
self.client_secret = generate_client_secret()
super().setUp()
def get_container_specs(self) -> Optional[Dict[str, Any]]:
return {
"image": "grafana/grafana:7.1.0",
"detach": True,
"network_mode": "host",
"auto_remove": True,
"healthcheck": Healthcheck(
test=["CMD", "wget", "--spider", "http://localhost:3000"],
interval=5 * 100 * 1000000,
start_period=1 * 100 * 1000000,
),
"environment": {
"GF_AUTH_GENERIC_OAUTH_ENABLED": "true",
"GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id,
"GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret,
"GF_AUTH_GENERIC_OAUTH_SCOPES": "openid email profile",
"GF_AUTH_GENERIC_OAUTH_AUTH_URL": (
self.url("passbook_providers_oauth2:authorize")
),
"GF_AUTH_GENERIC_OAUTH_TOKEN_URL": (
self.url("passbook_providers_oauth2:token")
),
"GF_AUTH_GENERIC_OAUTH_API_URL": (
self.url("passbook_providers_oauth2:userinfo")
),
"GF_AUTH_SIGNOUT_REDIRECT_URL": (
self.url(
"passbook_providers_oauth2:end-session",
application_slug=APPLICATION_SLUG,
)
),
"GF_LOG_LEVEL": "debug",
},
}
def test_redirect_uri_error(self):
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
sleep(1)
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
provider = OAuth2Provider.objects.create(
name="grafana",
client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
rsa_key=CertificateKeyPair.objects.first(),
redirect_uris="http://localhost:3000/",
authorization_flow=authorization_flow,
response_type=ResponseTypes.CODE,
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
)
)
provider.save()
Application.objects.create(
name="Grafana",
slug=APPLICATION_SLUG,
provider=provider,
)
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
sleep(2)
self.assertEqual(
self.driver.find_element(By.CLASS_NAME, "pf-c-title").text,
"Redirect URI Error",
)
def test_authorization_consent_implied(self):
"""test OpenID Provider flow (default authorization flow with implied consent)"""
sleep(1)
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
provider = OAuth2Provider.objects.create(
name="grafana",
client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
rsa_key=CertificateKeyPair.objects.first(),
redirect_uris="http://localhost:3000/login/generic_oauth",
authorization_flow=authorization_flow,
response_type=ResponseTypes.CODE,
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
)
)
provider.save()
Application.objects.create(
name="Grafana",
slug=APPLICATION_SLUG,
provider=provider,
)
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url("http://localhost:3000/?orgId=1")
self.driver.get("http://localhost:3000/profile")
self.assertEqual(
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
USER().name,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
"value"
),
USER().name,
)
self.assertEqual(
self.driver.find_element(
By.CSS_SELECTOR, "input[name=email]"
).get_attribute("value"),
USER().email,
)
self.assertEqual(
self.driver.find_element(
By.CSS_SELECTOR, "input[name=login]"
).get_attribute("value"),
USER().email,
)
def test_authorization_logout(self):
"""test OpenID Provider flow with logout"""
sleep(1)
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
provider = OAuth2Provider.objects.create(
name="grafana",
client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
rsa_key=CertificateKeyPair.objects.first(),
redirect_uris="http://localhost:3000/login/generic_oauth",
authorization_flow=authorization_flow,
response_type=ResponseTypes.CODE,
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
)
)
provider.save()
Application.objects.create(
name="Grafana",
slug=APPLICATION_SLUG,
provider=provider,
)
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url("http://localhost:3000/?orgId=1")
self.driver.get("http://localhost:3000/profile")
self.assertEqual(
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
USER().name,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
"value"
),
USER().name,
)
self.assertEqual(
self.driver.find_element(
By.CSS_SELECTOR, "input[name=email]"
).get_attribute("value"),
USER().email,
)
self.assertEqual(
self.driver.find_element(
By.CSS_SELECTOR, "input[name=login]"
).get_attribute("value"),
USER().email,
)
self.driver.get("http://localhost:3000/logout")
self.wait_for_url(
self.url(
"passbook_providers_oauth2:end-session",
application_slug=APPLICATION_SLUG,
)
)
self.driver.find_element(By.ID, "logout").click()
def test_authorization_consent_explicit(self):
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
sleep(1)
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-explicit-consent"
)
provider = OAuth2Provider.objects.create(
name="grafana",
authorization_flow=authorization_flow,
response_type=ResponseTypes.CODE,
client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
rsa_key=CertificateKeyPair.objects.first(),
redirect_uris="http://localhost:3000/login/generic_oauth",
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
)
)
provider.save()
app = Application.objects.create(
name="Grafana",
slug=APPLICATION_SLUG,
provider=provider,
)
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.assertEqual(
app.name,
self.driver.find_element(By.ID, "application-name").text,
)
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
)
sleep(1)
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
self.wait_for_url("http://localhost:3000/?orgId=1")
self.driver.get("http://localhost:3000/profile")
self.assertEqual(
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
USER().name,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
"value"
),
USER().name,
)
self.assertEqual(
self.driver.find_element(
By.CSS_SELECTOR, "input[name=email]"
).get_attribute("value"),
USER().email,
)
self.assertEqual(
self.driver.find_element(
By.CSS_SELECTOR, "input[name=login]"
).get_attribute("value"),
USER().email,
)
def test_authorization_denied(self):
"""test OpenID Provider flow (default authorization with access deny)"""
sleep(1)
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-explicit-consent"
)
provider = OAuth2Provider.objects.create(
name="grafana",
authorization_flow=authorization_flow,
response_type=ResponseTypes.CODE,
client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
rsa_key=CertificateKeyPair.objects.first(),
redirect_uris="http://localhost:3000/login/generic_oauth",
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
)
)
provider.save()
app = Application.objects.create(
name="Grafana",
slug=APPLICATION_SLUG,
provider=provider,
)
negative_policy = ExpressionPolicy.objects.create(
name="negative-static", expression="return False"
)
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
"Permission denied",
)

View File

@ -1,5 +1,8 @@
"""test OAuth2 OpenID Provider flow"""
from json import loads
from sys import platform
from time import sleep
from unittest.case import skipUnless
from docker import DockerClient, from_env
from docker.models.containers import Container
@ -34,43 +37,35 @@ from passbook.providers.oauth2.models import (
LOGGER = get_logger()
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestProviderOAuth2OIDC(SeleniumTestCase):
"""test OAuth with OpenID Provider flow"""
def setUp(self):
self.client_id = generate_client_id()
self.client_secret = generate_client_secret()
self.container = self.setup_client()
self.application_slug = "test"
super().setUp()
def setup_client(self) -> Container:
"""Setup client grafana container which we test OIDC against"""
"""Setup client saml-sp container which we test SAML against"""
sleep(1)
client: DockerClient = from_env()
client.images.pull("beryju/oidc-test-client")
container = client.containers.run(
image="grafana/grafana:7.1.0",
image="beryju/oidc-test-client",
detach=True,
network_mode="host",
auto_remove=True,
healthcheck=Healthcheck(
test=["CMD", "wget", "--spider", "http://localhost:3000"],
test=["CMD", "wget", "--spider", "http://localhost:9009/health"],
interval=5 * 100 * 1000000,
start_period=1 * 100 * 1000000,
),
environment={
"GF_AUTH_GENERIC_OAUTH_ENABLED": "true",
"GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id,
"GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret,
"GF_AUTH_GENERIC_OAUTH_SCOPES": "openid email profile",
"GF_AUTH_GENERIC_OAUTH_AUTH_URL": (
self.url("passbook_providers_oauth2:authorize")
),
"GF_AUTH_GENERIC_OAUTH_TOKEN_URL": (
self.url("passbook_providers_oauth2:token")
),
"GF_AUTH_GENERIC_OAUTH_API_URL": (
self.url("passbook_providers_oauth2:userinfo")
),
"GF_LOG_LEVEL": "debug",
"OIDC_CLIENT_ID": self.client_id,
"OIDC_CLIENT_SECRET": self.client_secret,
"OIDC_PROVIDER": f"{self.live_server_url}/application/o/{self.application_slug}/",
},
)
while True:
@ -81,10 +76,6 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
LOGGER.info("Container failed healthcheck")
sleep(1)
def tearDown(self):
self.container.kill()
super().tearDown()
def test_redirect_uri_error(self):
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
sleep(1)
@ -93,12 +84,12 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
slug="default-provider-authorization-implicit-consent"
)
provider = OAuth2Provider.objects.create(
name="grafana",
name=self.application_slug,
client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
rsa_key=CertificateKeyPair.objects.first(),
redirect_uris="http://localhost:3000/",
redirect_uris="http://localhost:9009/",
authorization_flow=authorization_flow,
response_type=ResponseTypes.CODE,
)
@ -109,11 +100,14 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
)
provider.save()
Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
name=self.application_slug,
slug=self.application_slug,
provider=provider,
)
self.container = self.setup_client()
self.driver.get("http://localhost:9009")
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
@ -133,12 +127,12 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
slug="default-provider-authorization-implicit-consent"
)
provider = OAuth2Provider.objects.create(
name="grafana",
name=self.application_slug,
client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
rsa_key=CertificateKeyPair.objects.first(),
redirect_uris="http://localhost:3000/login/generic_oauth",
redirect_uris="http://localhost:9009/auth/callback",
authorization_flow=authorization_flow,
response_type=ResponseTypes.CODE,
)
@ -149,39 +143,31 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
)
provider.save()
Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
name=self.application_slug,
slug=self.application_slug,
provider=provider,
)
self.container = self.setup_client()
self.driver.get("http://localhost:9009")
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
self.assertEqual(
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
USER().name,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
"value"
),
USER().name,
)
self.assertEqual(
self.driver.find_element(
By.CSS_SELECTOR, "input[name=email]"
).get_attribute("value"),
USER().email,
)
self.assertEqual(
self.driver.find_element(
By.CSS_SELECTOR, "input[name=login]"
).get_attribute("value"),
USER().email,
)
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre")))
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
self.assertEqual(body["IDTokenClaims"]["nickname"], USER().username)
self.assertEqual(body["UserInfo"]["nickname"], USER().username)
self.assertEqual(body["IDTokenClaims"]["name"], USER().name)
self.assertEqual(body["UserInfo"]["name"], USER().name)
self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
self.assertEqual(body["UserInfo"]["email"], USER().email)
def test_authorization_consent_explicit(self):
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
@ -191,14 +177,14 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
slug="default-provider-authorization-explicit-consent"
)
provider = OAuth2Provider.objects.create(
name="grafana",
name=self.application_slug,
authorization_flow=authorization_flow,
response_type=ResponseTypes.CODE,
client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
rsa_key=CertificateKeyPair.objects.first(),
redirect_uris="http://localhost:3000/login/generic_oauth",
redirect_uris="http://localhost:9009/auth/callback",
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
@ -207,22 +193,23 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
)
provider.save()
app = Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
name=self.application_slug,
slug=self.application_slug,
provider=provider,
)
self.container = self.setup_client()
self.driver.get("http://localhost:9009")
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.assertIn(
self.assertEqual(
app.name,
self.driver.find_element(
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
).text,
self.driver.find_element(By.ID, "application-name").text,
)
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
@ -230,34 +217,17 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
sleep(1)
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
self.wait.until(
ec.presence_of_element_located(
(By.XPATH, "//a[contains(@href, '/profile')]")
)
)
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
self.assertEqual(
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
USER().name,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
"value"
),
USER().name,
)
self.assertEqual(
self.driver.find_element(
By.CSS_SELECTOR, "input[name=email]"
).get_attribute("value"),
USER().email,
)
self.assertEqual(
self.driver.find_element(
By.CSS_SELECTOR, "input[name=login]"
).get_attribute("value"),
USER().email,
)
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre")))
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
self.assertEqual(body["IDTokenClaims"]["nickname"], USER().username)
self.assertEqual(body["UserInfo"]["nickname"], USER().username)
self.assertEqual(body["IDTokenClaims"]["name"], USER().name)
self.assertEqual(body["UserInfo"]["name"], USER().name)
self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
self.assertEqual(body["UserInfo"]["email"], USER().email)
def test_authorization_denied(self):
"""test OpenID Provider flow (default authorization with access deny)"""
@ -267,14 +237,14 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
slug="default-provider-authorization-explicit-consent"
)
provider = OAuth2Provider.objects.create(
name="grafana",
name=self.application_slug,
authorization_flow=authorization_flow,
response_type=ResponseTypes.CODE,
client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
rsa_key=CertificateKeyPair.objects.first(),
redirect_uris="http://localhost:3000/login/generic_oauth",
redirect_uris="http://localhost:9009/auth/callback",
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
@ -283,21 +253,28 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
)
provider.save()
app = Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
name=self.application_slug,
slug=self.application_slug,
provider=provider,
)
negative_policy = ExpressionPolicy.objects.create(
name="negative-static", expression="return False"
)
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
self.container = self.setup_client()
self.driver.get("http://localhost:9009")
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url(self.url("passbook_flows:denied"))
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
"Permission denied",

155
e2e/test_provider_proxy.py Normal file
View File

@ -0,0 +1,155 @@
"""Proxy and Outpost e2e tests"""
from dataclasses import asdict
from sys import platform
from time import sleep
from typing import Any, Dict, Optional
from unittest.case import skipUnless
from channels.testing import ChannelsLiveServerTestCase
from docker.client import DockerClient, from_env
from docker.models.containers import Container
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from e2e.utils import USER, SeleniumTestCase
from passbook import __version__
from passbook.core.models import Application
from passbook.flows.models import Flow
from passbook.outposts.models import (
Outpost,
OutpostConfig,
OutpostDeploymentType,
OutpostType,
)
from passbook.providers.proxy.models import ProxyProvider
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestProviderProxy(SeleniumTestCase):
"""Proxy and Outpost e2e tests"""
proxy_container: Container
def tearDown(self) -> None:
super().tearDown()
self.proxy_container.kill()
def get_container_specs(self) -> Optional[Dict[str, Any]]:
return {
"image": "traefik/whoami:latest",
"detach": True,
"network_mode": "host",
"auto_remove": True,
}
def start_proxy(self, outpost: Outpost) -> Container:
"""Start proxy container based on outpost created"""
client: DockerClient = from_env()
container = client.containers.run(
image=f"beryju/passbook-proxy:{__version__}",
detach=True,
network_mode="host",
auto_remove=True,
environment={
"PASSBOOK_HOST": self.live_server_url,
"PASSBOOK_TOKEN": outpost.token.key,
},
)
return container
def test_proxy_simple(self):
"""Test simple outpost setup with single provider"""
proxy: ProxyProvider = ProxyProvider.objects.create(
name="proxy_provider",
authorization_flow=Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
),
internal_host="http://localhost:80",
external_host="http://localhost:4180",
)
# Ensure OAuth2 Params are set
proxy.set_oauth_defaults()
proxy.save()
# we need to create an application to actually access the proxy
Application.objects.create(name="proxy", slug="proxy", provider=proxy)
outpost: Outpost = Outpost.objects.create(
name="proxy_outpost",
type=OutpostType.PROXY,
deployment_type=OutpostDeploymentType.CUSTOM,
)
outpost.providers.add(proxy)
outpost.save()
self.proxy_container = self.start_proxy(outpost)
# Wait until outpost healthcheck succeeds
healthcheck_retries = 0
while healthcheck_retries < 50:
if len(outpost.state) > 0:
state = outpost.state[0]
if state.last_seen:
break
healthcheck_retries += 1
sleep(0.5)
self.driver.get("http://localhost:4180")
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
sleep(1)
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
self.assertIn("X-Forwarded-Preferred-Username: pbadmin", full_body_text)
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestProviderProxyConnect(ChannelsLiveServerTestCase):
"""Test Proxy connectivity over websockets"""
def test_proxy_connectivity(self):
"""Test proxy connectivity over websocket"""
SeleniumTestCase().apply_default_data()
proxy: ProxyProvider = ProxyProvider.objects.create(
name="proxy_provider",
authorization_flow=Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
),
internal_host="http://localhost:80",
external_host="http://localhost:4180",
)
# Ensure OAuth2 Params are set
proxy.set_oauth_defaults()
proxy.save()
# we need to create an application to actually access the proxy
Application.objects.create(name="proxy", slug="proxy", provider=proxy)
outpost: Outpost = Outpost.objects.create(
name="proxy_outpost",
type=OutpostType.PROXY,
deployment_type=OutpostDeploymentType.DOCKER,
_config=asdict(
OutpostConfig(passbook_host=self.live_server_url, log_level="debug")
),
)
outpost.providers.add(proxy)
outpost.save()
# Wait until outpost healthcheck succeeds
healthcheck_retries = 0
while healthcheck_retries < 50:
if len(outpost.state) > 0:
state = outpost.state[0]
if state.last_seen and state.version:
break
healthcheck_retries += 1
sleep(0.5)
state = outpost.state
self.assertTrue(len(state), 1)
self.assertEqual(state[0].version, __version__)
# Make sure to delete the outpost to remove the container
outpost.delete()

View File

@ -1,11 +1,15 @@
"""test SAML Provider flow"""
from json import loads
from sys import platform
from time import sleep
from unittest.case import skipUnless
from docker import DockerClient, from_env
from docker.models.containers import Container
from docker.types import Healthcheck
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
from structlog import get_logger
from e2e.utils import USER, SeleniumTestCase
@ -23,6 +27,7 @@ from passbook.providers.saml.models import (
LOGGER = get_logger()
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestProviderSAML(SeleniumTestCase):
"""test SAML Provider flow"""
@ -31,6 +36,7 @@ class TestProviderSAML(SeleniumTestCase):
def setup_client(self, provider: SAMLProvider) -> Container:
"""Setup client saml-sp container which we test SAML against"""
client: DockerClient = from_env()
client.images.pull("beryju/oidc-test-client")
container = client.containers.run(
image="beryju/saml-test-sp",
detach=True,
@ -60,10 +66,6 @@ class TestProviderSAML(SeleniumTestCase):
LOGGER.info("Container failed healthcheck")
sleep(1)
def tearDown(self):
self.container.kill()
super().tearDown()
def test_sp_initiated_implicit(self):
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
# Bootstrap all needed objects
@ -82,7 +84,9 @@ class TestProviderSAML(SeleniumTestCase):
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
Application.objects.create(
name="SAML", slug="passbook-saml", provider=provider,
name="SAML",
slug="passbook-saml",
provider=provider,
)
self.container = self.setup_client(provider)
self.driver.get("http://localhost:9009")
@ -92,10 +96,14 @@ class TestProviderSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url("http://localhost:9009/")
self.assertEqual(
self.driver.find_element(By.XPATH, "/html/body/pre").text,
f"Hello, {USER().name}!",
)
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
self.assertEqual(body["attr"]["cn"], [USER().name])
self.assertEqual(body["attr"]["displayName"], [USER().username])
self.assertEqual(body["attr"]["eduPersonPrincipalName"], [USER().email])
self.assertEqual(body["attr"]["mail"], [USER().email])
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
def test_sp_initiated_explicit(self):
"""test SAML Provider flow SP-initiated flow (explicit consent)"""
@ -115,7 +123,9 @@ class TestProviderSAML(SeleniumTestCase):
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
app = Application.objects.create(
name="SAML", slug="passbook-saml", provider=provider,
name="SAML",
slug="passbook-saml",
provider=provider,
)
self.container = self.setup_client(provider)
self.driver.get("http://localhost:9009")
@ -124,19 +134,21 @@ class TestProviderSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.assertIn(
self.assertEqual(
app.name,
self.driver.find_element(
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
).text,
self.driver.find_element(By.ID, "application-name").text,
)
sleep(1)
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
self.wait_for_url("http://localhost:9009/")
self.assertEqual(
self.driver.find_element(By.XPATH, "/html/body/pre").text,
f"Hello, {USER().name}!",
)
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
self.assertEqual(body["attr"]["cn"], [USER().name])
self.assertEqual(body["attr"]["displayName"], [USER().username])
self.assertEqual(body["attr"]["eduPersonPrincipalName"], [USER().email])
self.assertEqual(body["attr"]["mail"], [USER().email])
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
def test_idp_initiated_implicit(self):
"""test SAML Provider flow IdP-initiated flow (implicit consent)"""
@ -156,7 +168,9 @@ class TestProviderSAML(SeleniumTestCase):
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
Application.objects.create(
name="SAML", slug="passbook-saml", provider=provider,
name="SAML",
slug="passbook-saml",
provider=provider,
)
self.container = self.setup_client(provider)
self.driver.get(
@ -170,11 +184,16 @@ class TestProviderSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
sleep(1)
self.wait_for_url("http://localhost:9009/")
self.assertEqual(
self.driver.find_element(By.XPATH, "/html/body/pre").text,
f"Hello, {USER().name}!",
)
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
self.assertEqual(body["attr"]["cn"], [USER().name])
self.assertEqual(body["attr"]["displayName"], [USER().username])
self.assertEqual(body["attr"]["eduPersonPrincipalName"], [USER().email])
self.assertEqual(body["attr"]["mail"], [USER().email])
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
def test_sp_initiated_denied(self):
"""test SAML Provider flow SP-initiated flow (Policy denies access)"""
@ -197,7 +216,9 @@ class TestProviderSAML(SeleniumTestCase):
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
app = Application.objects.create(
name="SAML", slug="passbook-saml", provider=provider,
name="SAML",
slug="passbook-saml",
provider=provider,
)
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
self.container = self.setup_client(provider)
@ -207,7 +228,10 @@ class TestProviderSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url(self.url("passbook_flows:denied"))
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
"Permission denied",

339
e2e/test_source_oauth.py Normal file
View File

@ -0,0 +1,339 @@
"""test OAuth Source"""
from os.path import abspath
from sys import platform
from time import sleep
from typing import Any, Dict, Optional
from unittest.case import skipUnless
from django.test import override_settings
from docker.models.containers import Container
from docker.types import Healthcheck
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
from structlog import get_logger
from yaml import safe_dump
from e2e.utils import SeleniumTestCase
from passbook.flows.models import Flow
from passbook.providers.oauth2.generators import (
generate_client_id,
generate_client_secret,
)
from passbook.sources.oauth.models import OAuthSource
CONFIG_PATH = "/tmp/dex.yml"
LOGGER = get_logger()
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestSourceOAuth2(SeleniumTestCase):
"""test OAuth Source flow"""
container: Container
def setUp(self):
self.client_secret = generate_client_secret()
self.prepare_dex_config()
super().setUp()
def prepare_dex_config(self):
"""Since Dex does not document which environment
variables can be used to configure clients"""
config = {
"enablePasswordDB": True,
"issuer": "http://127.0.0.1:5556/dex",
"logger": {"level": "debug"},
"staticClients": [
{
"id": "example-app",
"name": "Example App",
"redirectURIs": [
self.url(
"passbook_sources_oauth:oauth-client-callback",
source_slug="dex",
)
],
"secret": self.client_secret,
}
],
"staticPasswords": [
{
"email": "admin@example.com",
# hash for password
"hash": "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W",
"userID": "08a8684b-db88-4b73-90a9-3cd1661f5466",
"username": "admin",
}
],
"storage": {"config": {"file": "/tmp/dex.db"}, "type": "sqlite3"},
"web": {"http": "0.0.0.0:5556"},
}
with open(CONFIG_PATH, "w+") as _file:
safe_dump(config, _file)
def get_container_specs(self) -> Optional[Dict[str, Any]]:
return {
"image": "quay.io/dexidp/dex:v2.24.0",
"detach": True,
"network_mode": "host",
"auto_remove": True,
"command": "serve /config.yml",
"healthcheck": Healthcheck(
test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
interval=5 * 100 * 1000000,
start_period=1 * 100 * 1000000,
),
"volumes": {abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro"}},
}
def create_objects(self):
"""Create required objects"""
# Bootstrap all needed objects
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
OAuthSource.objects.create( # nosec
name="dex",
slug="dex",
authentication_flow=authentication_flow,
enrollment_flow=enrollment_flow,
provider_type="openid-connect",
authorization_url="http://127.0.0.1:5556/dex/auth",
access_token_url="http://127.0.0.1:5556/dex/token",
profile_url="http://127.0.0.1:5556/dex/userinfo",
consumer_key="example-app",
consumer_secret=self.client_secret,
)
def test_oauth_enroll(self):
"""test OAuth Source With With OIDC"""
self.create_objects()
self.driver.get(self.live_server_url)
self.wait.until(
ec.presence_of_element_located(
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
)
)
self.driver.find_element(
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
).click()
# Now we should be at the IDP, wait for the login field
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
self.driver.find_element(By.ID, "password").send_keys("password")
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
)
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
self.wait.until(ec.presence_of_element_located((By.NAME, "username")))
# At this point we've been redirected back
# and we're asked for the username
self.driver.find_element(By.NAME, "username").click()
self.driver.find_element(By.NAME, "username").send_keys("foo")
self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER)
# Wait until we've loaded the user info page
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
self.driver.get(self.url("passbook_core:user-settings"))
self.assertEqual(
self.driver.find_element(By.ID, "user-settings").text,
"foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
"admin",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
"admin@example.com",
)
@override_settings(SESSION_COOKIE_SAMESITE="strict")
def test_oauth_samesite_strict(self):
"""test OAuth Source With SameSite set to strict
(=will fail because session is not carried over)"""
self.create_objects()
self.driver.get(self.live_server_url)
self.wait.until(
ec.presence_of_element_located(
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
)
)
self.driver.find_element(
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
).click()
# Now we should be at the IDP, wait for the login field
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
self.driver.find_element(By.ID, "password").send_keys("password")
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
)
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, ".pf-c-alert__title"))
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-alert__title").text,
"Authentication Failed.",
)
def test_oauth_enroll_auth(self):
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
self.test_oauth_enroll()
# We're logged in at the end of this, log out and re-login
self.driver.find_element(By.ID, "logout").click()
self.wait.until(
ec.presence_of_element_located(
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
)
)
sleep(1)
self.driver.find_element(
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
).click()
sleep(1)
# Now we should be at the IDP, wait for the login field
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
self.driver.find_element(By.ID, "password").send_keys("password")
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
)
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
self.driver.get(self.url("passbook_core:user-settings"))
self.assertEqual(
self.driver.find_element(By.ID, "user-settings").text,
"foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
"admin",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
"admin@example.com",
)
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestSourceOAuth1(SeleniumTestCase):
"""Test OAuth1 Source"""
def setUp(self) -> None:
self.client_id = generate_client_id()
self.client_secret = generate_client_secret()
self.source_slug = "oauth1-test"
super().setUp()
def get_container_specs(self) -> Optional[Dict[str, Any]]:
return {
"image": "beryju/oauth1-test-server",
"detach": True,
"network_mode": "host",
"auto_remove": True,
"environment": {
"OAUTH1_CLIENT_ID": self.client_id,
"OAUTH1_CLIENT_SECRET": self.client_secret,
"OAUTH1_REDIRECT_URI": (
self.url(
"passbook_sources_oauth:oauth-client-callback",
source_slug=self.source_slug,
)
),
},
}
def create_objects(self):
"""Create required objects"""
# Bootstrap all needed objects
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
OAuthSource.objects.create( # nosec
name="oauth1",
slug=self.source_slug,
authentication_flow=authentication_flow,
enrollment_flow=enrollment_flow,
provider_type="twitter",
request_token_url="http://localhost:5000/oauth/request_token",
access_token_url="http://localhost:5000/oauth/access_token",
authorization_url="http://localhost:5000/oauth/authorize",
profile_url="http://localhost:5000/api/me",
consumer_key=self.client_id,
consumer_secret=self.client_secret,
)
def test_oauth_enroll(self):
"""test OAuth Source With With OIDC"""
self.create_objects()
self.driver.get(self.live_server_url)
self.wait.until(
ec.presence_of_element_located(
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
)
)
self.driver.find_element(
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
).click()
# Now we should be at the IDP, wait for the login field
self.wait.until(ec.presence_of_element_located((By.NAME, "username")))
self.driver.find_element(By.NAME, "username").send_keys("example-user")
self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER)
# Wait until we're logged in
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "[name='confirm']"))
)
self.driver.find_element(By.CSS_SELECTOR, "[name='confirm']").click()
# Wait until we've loaded the user info page
sleep(2)
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
self.driver.get(self.url("passbook_core:user-settings"))
self.assertEqual(
self.driver.find_element(By.ID, "user-settings").text,
"example-user",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"),
"example-user",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
"test name",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
"foo@example.com",
)

View File

@ -1,8 +1,9 @@
"""test SAML Source"""
from sys import platform
from time import sleep
from typing import Any, Dict, Optional
from unittest.case import skipUnless
from docker import DockerClient, from_env
from docker.models.containers import Container
from docker.types import Healthcheck
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
@ -68,53 +69,38 @@ Sm75WXsflOxuTn08LbgGc4s=
-----END PRIVATE KEY-----"""
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestSourceSAML(SeleniumTestCase):
"""test SAML Source flow"""
def setUp(self):
self.container = self.setup_client()
super().setUp()
def setup_client(self) -> Container:
"""Setup test IdP container"""
client: DockerClient = from_env()
container = client.containers.run(
image="kristophjunge/test-saml-idp:1.15",
detach=True,
network_mode="host",
auto_remove=True,
healthcheck=Healthcheck(
def get_container_specs(self) -> Optional[Dict[str, Any]]:
return {
"image": "kristophjunge/test-saml-idp:1.15",
"detach": True,
"network_mode": "host",
"auto_remove": True,
"healthcheck": Healthcheck(
test=["CMD", "curl", "http://localhost:8080"],
interval=5 * 100 * 1000000,
start_period=1 * 100 * 1000000,
),
environment={
"environment": {
"SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id",
"SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE": (
f"{self.live_server_url}/source/saml/saml-idp-test/acs/"
),
},
)
while True:
container.reload()
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
if status == "healthy":
return container
LOGGER.info("Container failed healthcheck")
sleep(1)
def tearDown(self):
self.container.kill()
super().tearDown()
}
def test_idp_redirect(self):
"""test SAML Source With redirect binding"""
sleep(1)
# Bootstrap all needed objects
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
keypair = CertificateKeyPair.objects.create(
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
name="test-idp-cert",
certificate_data=IDP_CERT,
key_data=IDP_KEY,
)
SAMLSource.objects.create(
@ -146,12 +132,8 @@ class TestSourceSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
self.wait.until(
ec.presence_of_element_located(
(By.XPATH, "//a[contains(@href, '/-/user/')]")
)
)
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
self.driver.get(self.url("passbook_core:user-settings"))
# Wait until we've loaded the user info page
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
@ -161,12 +143,13 @@ class TestSourceSAML(SeleniumTestCase):
def test_idp_post(self):
"""test SAML Source With post binding"""
sleep(1)
# Bootstrap all needed objects
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
keypair = CertificateKeyPair.objects.create(
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
name="test-idp-cert",
certificate_data=IDP_CERT,
key_data=IDP_KEY,
)
SAMLSource.objects.create(
@ -200,12 +183,8 @@ class TestSourceSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
self.wait.until(
ec.presence_of_element_located(
(By.XPATH, "//a[contains(@href, '/-/user/')]")
)
)
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
self.driver.get(self.url("passbook_core:user-settings"))
# Wait until we've loaded the user info page
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
@ -215,12 +194,13 @@ class TestSourceSAML(SeleniumTestCase):
def test_idp_post_auto(self):
"""test SAML Source With post binding (auto redirect)"""
sleep(1)
# Bootstrap all needed objects
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
keypair = CertificateKeyPair.objects.create(
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
name="test-idp-cert",
certificate_data=IDP_CERT,
key_data=IDP_KEY,
)
SAMLSource.objects.create(
@ -252,12 +232,8 @@ class TestSourceSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
self.wait.until(
ec.presence_of_element_located(
(By.XPATH, "//a[contains(@href, '/-/user/')]")
)
)
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
self.driver.get(self.url("passbook_core:user-settings"))
# Wait until we've loaded the user info page
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))

View File

@ -1,215 +0,0 @@
"""test OAuth Source"""
from os.path import abspath
from time import sleep
from docker import DockerClient, from_env
from docker.models.containers import Container
from docker.types import Healthcheck
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
from structlog import get_logger
from yaml import safe_dump
from e2e.utils import SeleniumTestCase
from passbook.flows.models import Flow
from passbook.providers.oauth2.generators import generate_client_secret
from passbook.sources.oauth.models import OAuthSource
TOKEN_URL = "http://127.0.0.1:5556/dex/token"
CONFIG_PATH = "/tmp/dex.yml"
LOGGER = get_logger()
class TestSourceOAuth(SeleniumTestCase):
"""test OAuth Source flow"""
container: Container
def setUp(self):
self.client_secret = generate_client_secret()
self.container = self.setup_client()
super().setUp()
def prepare_dex_config(self):
"""Since Dex does not document which environment
variables can be used to configure clients"""
config = {
"enablePasswordDB": True,
"issuer": "http://127.0.0.1:5556/dex",
"logger": {"level": "debug"},
"staticClients": [
{
"id": "example-app",
"name": "Example App",
"redirectURIs": [
self.url(
"passbook_sources_oauth:oauth-client-callback",
source_slug="dex",
)
],
"secret": self.client_secret,
}
],
"staticPasswords": [
{
"email": "admin@example.com",
# hash for password
"hash": "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W",
"userID": "08a8684b-db88-4b73-90a9-3cd1661f5466",
"username": "admin",
}
],
"storage": {"config": {"file": "/tmp/dex.db"}, "type": "sqlite3"},
"web": {"http": "0.0.0.0:5556"},
}
with open(CONFIG_PATH, "w+") as _file:
safe_dump(config, _file)
def setup_client(self) -> Container:
"""Setup test Dex container"""
self.prepare_dex_config()
client: DockerClient = from_env()
container = client.containers.run(
image="quay.io/dexidp/dex:v2.24.0",
detach=True,
network_mode="host",
auto_remove=True,
command="serve /config.yml",
healthcheck=Healthcheck(
test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
interval=5 * 100 * 1000000,
start_period=1 * 100 * 1000000,
),
volumes={abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro"}},
)
while True:
container.reload()
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
if status == "healthy":
return container
LOGGER.info("Container failed healthcheck")
sleep(1)
def create_objects(self):
"""Create required objects"""
sleep(1)
# Bootstrap all needed objects
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
OAuthSource.objects.create(
name="dex",
slug="dex",
authentication_flow=authentication_flow,
enrollment_flow=enrollment_flow,
provider_type="openid-connect",
authorization_url="http://127.0.0.1:5556/dex/auth",
access_token_url=TOKEN_URL,
profile_url="http://127.0.0.1:5556/dex/userinfo",
consumer_key="example-app",
consumer_secret=self.client_secret,
)
def tearDown(self):
self.container.kill()
super().tearDown()
def test_oauth_enroll(self):
"""test OAuth Source With With OIDC"""
self.create_objects()
self.driver.get(self.live_server_url)
self.wait.until(
ec.presence_of_element_located(
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
)
)
self.driver.find_element(
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
).click()
# Now we should be at the IDP, wait for the login field
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
self.driver.find_element(By.ID, "password").send_keys("password")
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
)
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
# At this point we've been redirected back
# and we're asked for the username
self.driver.find_element(By.NAME, "username").click()
self.driver.find_element(By.NAME, "username").send_keys("foo")
self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER)
# Wait until we've loaded the user info page
self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
self.driver.find_element(By.LINK_TEXT, "foo").click()
self.wait_for_url(self.url("passbook_core:user-settings"))
self.assertEqual(
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
"foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"), "admin",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
"admin@example.com",
)
def test_oauth_enroll_auth(self):
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
self.test_oauth_enroll()
# We're logged in at the end of this, log out and re-login
self.driver.find_element(By.CSS_SELECTOR, "[aria-label=logout]").click()
self.wait.until(
ec.presence_of_element_located(
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
)
)
self.driver.find_element(
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
).click()
# Now we should be at the IDP, wait for the login field
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
self.driver.find_element(By.ID, "password").send_keys("password")
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
)
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
# Wait until we've loaded the user info page
self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
self.driver.find_element(By.LINK_TEXT, "foo").click()
self.wait_for_url(self.url("passbook_core:user-settings"))
self.assertEqual(
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
"foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"), "admin",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
"admin@example.com",
)

View File

@ -1,16 +1,18 @@
"""passbook e2e testing utilities"""
from functools import lru_cache
from glob import glob
from importlib.util import module_from_spec, spec_from_file_location
from inspect import getmembers, isfunction
from os import environ, makedirs
from time import time
from time import sleep, time
from typing import Any, Dict, Optional
from django.apps import apps
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.db import connection, transaction
from django.db.utils import IntegrityError
from django.shortcuts import reverse
from docker import DockerClient, from_env
from docker.models.containers import Container
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.remote.webdriver import WebDriver
@ -20,7 +22,6 @@ from structlog import get_logger
from passbook.core.models import User
@lru_cache
# pylint: disable=invalid-name
def USER() -> User: # noqa
"""Cached function that always returns pbadmin"""
@ -30,15 +31,38 @@ def USER() -> User: # noqa
class SeleniumTestCase(StaticLiveServerTestCase):
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
container: Optional[Container] = None
def setUp(self):
super().setUp()
makedirs("selenium_screenshots/", exist_ok=True)
self.driver = self._get_driver()
self.driver.maximize_window()
self.driver.implicitly_wait(30)
self.wait = WebDriverWait(self.driver, 50)
self.wait = WebDriverWait(self.driver, 60)
self.apply_default_data()
self.logger = get_logger()
if specs := self.get_container_specs():
self.container = self._start_container(specs)
def _start_container(self, specs: Dict[str, Any]) -> Container:
client: DockerClient = from_env()
client.images.pull(specs["image"])
container = client.containers.run(**specs)
if "healthcheck" not in specs:
return container
while True:
container.reload()
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
if status == "healthy":
return container
self.logger.info("Container failed healthcheck")
sleep(1)
def get_container_specs(self) -> Optional[Dict[str, Any]]:
"""Optionally get container specs which will launched on setup, wait for the container to
be healthy, and deleted again on tearDown"""
return None
def _get_driver(self) -> WebDriver:
return webdriver.Remote(
@ -57,6 +81,8 @@ class SeleniumTestCase(StaticLiveServerTestCase):
self.logger.warning(
line["message"], source=line["source"], level=line["level"]
)
if self.container:
self.container.kill()
self.driver.quit()
super().tearDown()

View File

@ -1,15 +1,15 @@
apiVersion: v2
appVersion: "0.10.0-rc4"
appVersion: "0.12.2-stable"
description: A Helm chart for passbook.
name: passbook
version: "0.10.0-rc4"
icon: https://github.com/BeryJu/passbook/blob/master/passbook/static/static/passbook/logo.svg
version: "0.12.2-stable"
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
dependencies:
- name: postgresql
version: 9.3.2
version: 9.4.1
repository: https://charts.bitnami.com/bitnami
condition: install.postgresql
- name: redis
version: 10.7.16
version: 10.9.0
repository: https://charts.bitnami.com/bitnami
condition: install.redis

View File

@ -7,13 +7,14 @@ data:
POSTGRESQL__NAME: "{{ .Values.postgresql.postgresqlDatabase }}"
POSTGRESQL__USER: "{{ .Values.postgresql.postgresqlUsername }}"
{{- if .Values.backup }}
POSTGRESQL__BACKUP__ACCESS_KEY: "{{ .Values.backup.access_key }}"
POSTGRESQL__BACKUP__SECRET_KEY: "{{ .Values.backup.secret_key }}"
POSTGRESQL__BACKUP__BUCKET: "{{ .Values.backup.bucket }}"
POSTGRESQL__BACKUP__HOST: "{{ .Values.backup.host }}"
POSTGRESQL__S3_BACKUP__ACCESS_KEY: "{{ .Values.backup.accessKey }}"
POSTGRESQL__S3_BACKUP__SECRET_KEY: "{{ .Values.backup.secretKey }}"
POSTGRESQL__S3_BACKUP__BUCKET: "{{ .Values.backup.bucket }}"
POSTGRESQL__S3_BACKUP__REGION: "{{ .Values.backup.region }}"
POSTGRESQL__S3_BACKUP__HOST: "{{ .Values.backup.host }}"
{{- end}}
REDIS__HOST: "{{ .Release.Name }}-redis-master"
ERROR_REPORTING__ENABLED: "{{ .Values.config.error_reporting.enabled }}"
ERROR_REPORTING__ENVIRONMENT: "{{ .Values.config.error_reporting.environment }}"
ERROR_REPORTING__SEND_PII: "{{ .Values.config.error_reporting.send_pii }}"
LOG_LEVEL: "{{ .Values.config.log_level }}"
ERROR_REPORTING__ENABLED: "{{ .Values.config.errorReporting.enabled }}"
ERROR_REPORTING__ENVIRONMENT: "{{ .Values.config.errorReporting.environment }}"
ERROR_REPORTING__SEND_PII: "{{ .Values.config.errorReporting.sendPii }}"
LOG_LEVEL: "{{ .Values.config.logLevel }}"

View File

@ -0,0 +1,42 @@
{{- if .Values.backup }}
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: {{ include "passbook.fullname" . }}-backup
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
helm.sh/chart: {{ include "passbook.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
schedule: "0 0 * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
args: [server]
envFrom:
- configMapRef:
name: {{ include "passbook.fullname" . }}-config
prefix: PASSBOOK_
env:
- name: PASSBOOK_SECRET_KEY
valueFrom:
secretKeyRef:
name: "{{ include "passbook.fullname" . }}-secret-key"
key: "secret_key"
- name: PASSBOOK_REDIS__PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-redis"
key: "redis-password"
- name: PASSBOOK_POSTGRESQL__PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-postgresql"
key: "postgresql-password"
{{- end}}

View File

@ -5,8 +5,8 @@ metadata:
name: {{ include "passbook.fullname" . }}-secret-key
data:
monitoring_username: bW9uaXRvcg== # monitor in base64
{{- if .Values.config.secret_key }}
secret_key: {{ .Values.config.secret_key | b64enc | quote }}
{{- if .Values.config.secretKey }}
secret_key: {{ .Values.config.secretKey | b64enc | quote }}
{{- else }}
secret_key: {{ randAlphaNum 50 | b64enc | quote}}
{{- end }}

View File

@ -0,0 +1,64 @@
{{- if .Values.kubernetesIntegration }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "passbook.fullname" . }}-sa-role
rules:
- apiGroups:
- ""
resources:
- secrets
- services
verbs:
- "get"
- "create"
- "delete"
- "read"
- "patch"
- apiGroups:
- "extensions"
- "apps"
resources:
- "deployments"
verbs:
- "get"
- "create"
- "delete"
- "read"
- "patch"
- apiGroups:
- "extensions"
- "networking.k8s.io"
resources:
- "ingresses"
verbs:
- "get"
- "create"
- "delete"
- "read"
- "patch"
- apiGroups:
- ""
resources:
- namespaces
verbs:
- list
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "passbook.fullname" . }}-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "passbook.fullname" . }}-sa-role-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "passbook.fullname" . }}-sa-role
subjects:
- kind: ServiceAccount
name: {{ include "passbook.fullname" . }}-sa
namespace: {{ .Release.Namespace }}
{{- end }}

View File

@ -9,7 +9,7 @@ metadata:
app.kubernetes.io/managed-by: {{ .Release.Service }}
k8s.passbook.beryju.org/component: web
spec:
replicas: {{ serverReplicas }}
replicas: {{ .Values.serverReplicas }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
@ -22,10 +22,29 @@ spec:
app.kubernetes.io/instance: {{ .Release.Name }}
k8s.passbook.beryju.org/component: web
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- {{ include "passbook.name" . }}
- key: app.kubernetes.io/instance
operator: In
values:
- {{ .Release.Name }}
- key: k8s.passbook.beryju.org/component
operator: In
values:
- web
topologyKey: "kubernetes.io/hostname"
initContainers:
- name: passbook-database-migrations
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
imagePullPolicy: Always
args: [migrate]
envFrom:
- configMapRef:
@ -50,7 +69,6 @@ spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
imagePullPolicy: Always
args: [server]
envFrom:
- configMapRef:
@ -82,18 +100,18 @@ spec:
port: http
httpHeaders:
- name: Host
value: kubernetes-healthcheck-host
value: passbook-healthcheck-host
readinessProbe:
httpGet:
path: /
port: http
httpHeaders:
- name: Host
value: kubernetes-healthcheck-host
value: passbook-healthcheck-host
resources:
requests:
cpu: 100m
memory: 200M
memory: 300M
limits:
cpu: 300m
memory: 350M
memory: 500M

View File

@ -9,7 +9,7 @@ metadata:
app.kubernetes.io/managed-by: {{ .Release.Service }}
k8s.passbook.beryju.org/component: worker
spec:
replicas: {{ workerReplicas }}
replicas: {{ .Values.workerReplicas }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
@ -22,6 +22,29 @@ spec:
app.kubernetes.io/instance: {{ .Release.Name }}
k8s.passbook.beryju.org/component: worker
spec:
{{- if .Values.kubernetesIntegration }}
serviceAccountName: {{ include "passbook.fullname" . }}-sa
{{- end }}
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- {{ include "passbook.name" . }}
- key: app.kubernetes.io/instance
operator: In
values:
- {{ .Release.Name }}
- key: k8s.passbook.beryju.org/component
operator: In
values:
- worker
topologyKey: "kubernetes.io/hostname"
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
@ -50,7 +73,7 @@ spec:
resources:
requests:
cpu: 150m
memory: 300M
memory: 400M
limits:
cpu: 300m
memory: 500M
memory: 600M

21
helm/values.test.yaml Normal file
View File

@ -0,0 +1,21 @@
image:
tag: gh-master
serverReplicas: 1
workerReplicas: 1
config:
# Log level used by web and worker
# Can be either debug, info, warning, error
logLevel: debug
ingress:
hosts:
- passbook.127.0.0.1.nip.io
# These values influence the bundled postgresql and redis charts, but are also used by passbook to connect
postgresql:
postgresqlPassword: EK-5jnKfjrGRm<77
redis:
password: password

View File

@ -4,32 +4,48 @@
image:
name: beryju/passbook
name_static: beryju/passbook-static
tag: 0.10.0-rc4
tag: 0.12.2-stable
nameOverride: ""
serverReplicas: 1
workerReplicas: 1
# Enable the Kubernetes integration which lets passbook deploy outposts into kubernetes
kubernetesIntegration: true
config:
# Optionally specify fixed secret_key, otherwise generated automatically
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
# secretKey: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
# Enable error reporting
error_reporting:
errorReporting:
enabled: false
environment: customer
send_pii: false
sendPii: false
# Log level used by web and worker
# Can be either debug, info, warning, error
log_level: warning
logLevel: warning
# Enable Database Backups to S3
# backup:
# access_key: access-key
# secret_key: secret-key
# accessKey: access-key
# secretKey: secret-key
# bucket: s3-bucket
# region: eu-central-1
# host: s3-host
ingress:
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
path: /
hosts:
- passbook.k8s.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - passbook.k8s.local
###################################
# Values controlling dependencies
###################################
@ -50,15 +66,3 @@ redis:
enabled: false
# https://stackoverflow.com/a/59189742
disableCommands: []
ingress:
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
path: /
hosts:
- passbook.k8s.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - passbook.k8s.local

View File

@ -1,14 +1,20 @@
#!/bin/bash -e
python -m lifecycle.wait_for_db
printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@"
printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@" > /dev/stderr
if [[ "$1" == "server" ]]; then
gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application
elif [[ "$1" == "worker" ]]; then
celery worker --autoscale=10,3 -E -B -A=passbook.root.celery -s=/tmp/celerybeat-schedule
celery -A passbook.root.celery worker --autoscale 3,1 -E -B -s /tmp/celerybeat-schedule -Q passbook,passbook_scheduled
elif [[ "$1" == "migrate" ]]; then
# Run system migrations first, run normal migrations after
python -m lifecycle.migrate
python -m manage migrate
elif [[ "$1" == "backup" ]]; then
python -m manage dbbackup --clean
elif [[ "$1" == "restore" ]]; then
python -m manage dbrestore ${@:2}
elif [[ "$1" == "bash" ]]; then
/bin/bash
else
python -m manage "$@"
fi

View File

@ -1,9 +1,10 @@
"""Gunicorn config"""
from multiprocessing import cpu_count
from pathlib import Path
import structlog
bind = "0.0.0.0:8000"
workers = 2
threads = 4
user = "passbook"
group = "passbook"
@ -40,3 +41,11 @@ logconfig_dict = {
"gunicorn": {"handlers": ["console"], "level": "INFO", "propagate": False},
},
}
# if we're running in kubernetes, use fixed workers because we can scale with more pods
# otherwise (assume docker-compose), use as much as we can
if Path("/var/run/secrets/kubernetes.io").exists():
workers = 2
else:
worker = cpu_count() * 2 + 1
threads = 4

View File

@ -47,7 +47,9 @@ if __name__ == "__main__":
# pyright: reportGeneralTypeIssues=false
spec.loader.exec_module(mod)
for _, sub in getmembers(mod, isclass):
for name, sub in getmembers(mod, isclass):
if name != "Migration":
continue
migration = sub(curr, conn)
if migration.needs_migration():
LOGGER.info("Migration needs to be applied", migration=sub)

View File

@ -25,7 +25,7 @@ delete from django_migrations where app = 'passbook_stages_password' and
name = '0002_passwordstage_change_flow';"""
class To010Migration(BaseMigration):
class Migration(BaseMigration):
def needs_migration(self) -> bool:
self.cur.execute(
"select * from information_schema.tables where table_name='oidc_provider_client'"

View File

@ -1,16 +1,28 @@
#!/usr/bin/env python
"""This file needs to be run from the root of the project to correctly
import passbook. This is done by the dockerfile."""
from json import dumps
from sys import stderr
from time import sleep
from psycopg2 import OperationalError, connect
from redis import Redis
from redis.exceptions import RedisError
from structlog import get_logger
from passbook.lib.config import CONFIG
LOGGER = get_logger()
def j_print(event: str, log_level: str = "info", **kwargs):
"""Print event in the same format as structlog with JSON.
Used before structlog is configured."""
data = {
"event": event,
"level": log_level,
"logger": __name__,
}
data.update(**kwargs)
print(dumps(data), file=stderr)
while True:
try:
@ -24,7 +36,7 @@ while True:
break
except OperationalError:
sleep(1)
LOGGER.warning("PostgreSQL Connection failed, retrying...")
j_print("PostgreSQL Connection failed, retrying...")
while True:
try:
@ -38,4 +50,4 @@ while True:
break
except RedisError:
sleep(1)
LOGGER.warning("Redis Connection failed, retrying...")
j_print("Redis Connection failed, retrying...")

View File

@ -8,29 +8,34 @@ nav:
- Installation:
- docker-compose: installation/docker-compose.md
- Kubernetes: installation/kubernetes.md
- Reverse Proxy: installation/reverse-proxy.md
- Flows:
Overview: flow/flows.md
Examples: flow/examples/examples.md
- Stages:
- Captcha Stage: flow/stages/captcha/index.md
- Dummy Stage: flow/stages/dummy/index.md
- Email Stage: flow/stages/email/index.md
- Identification Stage: flow/stages/identification/index.md
- Invitation Stage: flow/stages/invitation/index.md
- OTP Stage: flow/stages/otp/index.md
- Password Stage: flow/stages/password/index.md
- Prompt Stage: flow/stages/prompt/index.md
- Prompt Stage Validation: flow/stages/prompt/validation.md
- User Delete Stage: flow/stages/user_delete.md
- User Login Stage: flow/stages/user_login.md
- User Logout Stage: flow/stages/user_logout.md
- User Write Stage: flow/stages/user_write.md
- Captcha Stage: flow/stages/captcha/index.md
- Dummy Stage: flow/stages/dummy/index.md
- Email Stage: flow/stages/email/index.md
- Identification Stage: flow/stages/identification/index.md
- Invitation Stage: flow/stages/invitation/index.md
- OTP Stage: flow/stages/otp/index.md
- Password Stage: flow/stages/password/index.md
- Prompt Stage: flow/stages/prompt/index.md
- Prompt Stage Validation: flow/stages/prompt/validation.md
- User Delete Stage: flow/stages/user_delete.md
- User Login Stage: flow/stages/user_login.md
- User Logout Stage: flow/stages/user_logout.md
- User Write Stage: flow/stages/user_write.md
- Sources: sources.md
- Providers:
- OAuth2: providers/oauth2.md
- SAML: providers/saml.md
- Proxy: providers/proxy.md
- Outposts: outposts/outposts.md
- Outposts:
- Overview: outposts/outposts.md
- Upgrading: outposts/upgrading.md
- Deploy on docker-compose: outposts/deploy-docker-compose.md
- Deploy on Kubernetes: outposts/deploy-kubernetes.md
- Expressions:
- Overview: expressions/index.md
- Reference:
@ -42,16 +47,28 @@ nav:
- Overview: policies/index.md
- Expression: policies/expression.md
- Integrations:
- as Source:
- Active Directory: integrations/sources/active-directory/index.md
- as Provider:
- Amazon Web Services: integrations/services/aws/index.md
- GitLab: integrations/services/gitlab/index.md
- Rancher: integrations/services/rancher/index.md
- Harbor: integrations/services/harbor/index.md
- Sentry: integrations/services/sentry/index.md
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
- Amazon Web Services: integrations/services/aws/index.md
- GitLab: integrations/services/gitlab/index.md
- Rancher: integrations/services/rancher/index.md
- Harbor: integrations/services/harbor/index.md
- Sentry: integrations/services/sentry/index.md
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
- VMware vCenter: integrations/services/vmware-vcenter/index.md
- Ubuntu Landscape: integrations/services/ubuntu-landscape/index.md
- Sonarr: integrations/services/sonarr/index.md
- Tautulli: integrations/services/tautulli/index.md
- Maintenance:
- Backups: maintenance/backups/index.md
- Upgrading:
- to 0.9: upgrading/to-0.9.md
- to 0.10: upgrading/to-0.10.md
- to 0.11: upgrading/to-0.11.md
- to 0.12: upgrading/to-0.12.md
- Troubleshooting:
- Access problems: troubleshooting/access.md
repo_name: "BeryJu/passbook"
repo_url: https://github.com/BeryJu/passbook

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = "0.10.0-rc4"
__version__ = "0.12.2-stable"

View File

View File

@ -0,0 +1,80 @@
"""passbook administration overview"""
from django.core.cache import cache
from django.http import response
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 passbook import __version__
from passbook.admin.tasks import VERSION_CACHE_KEY, update_latest_version
from passbook.core.models import Provider
from passbook.policies.models import Policy
from passbook.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

@ -0,0 +1,80 @@
"""passbook administration overview"""
import time
from collections import Counter
from datetime import timedelta
from typing import Dict, List
from django.db.models import Count, ExpressionWrapper, F
from django.db.models.fields import DurationField
from django.db.models.functions import ExtractHour
from django.http import response
from django.utils.timezone import now
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 passbook.audit.models import Event, EventAction
class AdministrationMetricsSerializer(Serializer):
"""Overview View"""
logins_per_1h = SerializerMethodField()
logins_failed_per_1h = SerializerMethodField()
def get_events_per_1h(self, action: str) -> List[Dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
date_from = now() - timedelta(days=1)
result = (
Event.objects.filter(action=action, created__gte=date_from)
.annotate(
age=ExpressionWrapper(
now() - F("created"), output_field=DurationField()
)
)
.annotate(age_hours=ExtractHour("age"))
.values("age_hours")
.annotate(count=Count("pk"))
.order_by("age_hours")
)
data = Counter({d["age_hours"]: d["count"] for d in result})
results = []
_now = now()
for hour in range(0, -24, -1):
results.append(
{
"x": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
"y": data[hour * -1],
}
)
return results
def get_logins_per_1h(self, _):
"""Get successful logins per hour for the last 24 hours"""
return self.get_events_per_1h(EventAction.LOGIN)
def get_logins_failed_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
return self.get_events_per_1h(EventAction.LOGIN_FAILED)
def create(self, request: Request) -> response:
raise NotImplementedError
def update(self, request: Request) -> Response:
raise NotImplementedError
class AdministrationMetricsViewSet(ViewSet):
"""Return single instance of AdministrationMetricsSerializer"""
permission_classes = [IsAdminUser]
@swagger_auto_schema(responses={200: AdministrationMetricsSerializer(many=True)})
def list(self, request: Request) -> Response:
"""Return single instance of AdministrationMetricsSerializer"""
serializer = AdministrationMetricsSerializer(True)
return Response(serializer.data)

View File

@ -0,0 +1,64 @@
"""Tasks API"""
from importlib import import_module
from django.contrib import messages
from django.http.response import Http404
from django.utils.translation import gettext_lazy as _
from drf_yasg2.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.fields import CharField, DateTimeField, IntegerField, ListField
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 passbook.lib.tasks import TaskInfo
class TaskSerializer(Serializer):
"""Serialize TaskInfo and TaskResult"""
task_name = CharField()
task_description = CharField()
task_finish_timestamp = DateTimeField(source="finish_timestamp")
status = IntegerField(source="result.status.value")
messages = ListField(source="result.messages")
def create(self, request: Request) -> Response:
raise NotImplementedError
def update(self, request: Request) -> Response:
raise NotImplementedError
class TaskViewSet(ViewSet):
"""Read-only view set that returns all background tasks"""
permission_classes = [IsAdminUser]
@swagger_auto_schema(responses={200: TaskSerializer(many=True)})
def list(self, request: Request) -> Response:
"""List current messages and pass into Serializer"""
return Response(TaskSerializer(TaskInfo.all().values(), many=True).data)
@action(detail=True, methods=["post"])
# pylint: disable=invalid-name
def retry(self, request: Request, pk=None) -> Response:
"""Retry task"""
task = TaskInfo.by_name(pk)
if not task:
raise Http404
task_module = import_module(task.task_call_module)
task_func = getattr(task_module, task.task_call_func)
task_func.delay(*task.task_call_args, **task.task_call_kwargs)
messages.success(
self.request,
_("Successfully re-scheduled Task %(name)s!" % {"name": task.task_name}),
)
return Response(
{
"successful": True,
}
)

View File

@ -1,9 +1,35 @@
"""YAML fields"""
"""Additional fields"""
import yaml
from django import forms
from django.utils.datastructures import MultiValueDict
from django.utils.translation import gettext_lazy as _
class ArrayFieldSelectMultiple(forms.SelectMultiple):
"""This is a Form Widget for use with a Postgres ArrayField. It implements
a multi-select interface that can be given a set of `choices`.
You can provide a `delimiter` keyword argument to specify the delimeter used.
https://gist.github.com/stephane/00e73c0002de52b1c601"""
def __init__(self, *args, **kwargs):
# Accept a `delimiter` argument, and grab it (defaulting to a comma)
self.delimiter = kwargs.pop("delimiter", ",")
super().__init__(*args, **kwargs)
def value_from_datadict(self, data, files, name):
if isinstance(data, MultiValueDict):
# Normally, we'd want a list here, which is what we get from the
# SelectMultiple superclass, but the SimpleArrayField expects to
# get a delimited string, so we're doing a little extra work.
return self.delimiter.join(data.getlist(name))
return data.get(name)
def get_context(self, name, value, attrs):
return super().get_context(name, value.split(self.delimiter), attrs)
class CodeMirrorWidget(forms.Textarea):
"""Custom Textarea-based Widget that triggers a CodeMirror editor"""
@ -15,11 +41,8 @@ class CodeMirrorWidget(forms.Textarea):
self.mode = mode
def render(self, *args, **kwargs):
if "attrs" not in kwargs:
kwargs["attrs"] = {}
attrs = kwargs["attrs"]
if "class" not in attrs:
attrs["class"] = ""
attrs = kwargs.setdefault("attrs", {})
attrs.setdefault("class", "")
attrs["class"] += " codemirror"
attrs["data-cm-mode"] = self.mode
return super().render(*args, **kwargs)
@ -52,10 +75,14 @@ class YAMLField(forms.JSONField):
converted = yaml.safe_load(value)
except yaml.YAMLError:
raise forms.ValidationError(
self.error_messages["invalid"], code="invalid", params={"value": value},
self.error_messages["invalid"],
code="invalid",
params={"value": value},
)
if isinstance(converted, str):
return YAMLString(converted)
if converted is None:
return {}
return converted
def bound_data(self, data, initial):

View File

@ -12,7 +12,7 @@ class UserForm(forms.ModelForm):
class Meta:
model = User
fields = ["username", "name", "email", "is_staff", "is_active", "attributes"]
fields = ["username", "name", "email", "is_active", "attributes"]
widgets = {
"name": forms.TextInput,
"attributes": CodeMirrorWidget,

View File

@ -1,26 +0,0 @@
"""passbook admin Middleware to impersonate users"""
from passbook.core.models import User
def impersonate(get_response):
"""Middleware to impersonate users"""
def middleware(request):
"""Middleware to impersonate users"""
# User is superuser and has __impersonate ID set
if request.user.is_superuser and "__impersonate" in request.GET:
request.session["impersonate_id"] = request.GET["__impersonate"]
# user wants to stop impersonation
elif "__unimpersonate" in request.GET and "impersonate_id" in request.session:
del request.session["impersonate_id"]
# Actually impersonate user
if request.user.is_superuser and "impersonate_id" in request.session:
request.user = User.objects.get(pk=request.session["impersonate_id"])
response = get_response(request)
return response
return middleware

View File

@ -1,5 +1,10 @@
"""passbook admin settings"""
from celery.schedules import crontab
MIDDLEWARE = [
"passbook.admin.middleware.impersonate",
]
CELERY_BEAT_SCHEDULE = {
"admin_latest_version": {
"task": "passbook.admin.tasks.update_latest_version",
"schedule": crontab(minute=0), # Run every hour
"options": {"queue": "passbook_scheduled"},
}
}

30
passbook/admin/tasks.py Normal file
View File

@ -0,0 +1,30 @@
"""passbook admin tasks"""
from django.core.cache import cache
from requests import RequestException, get
from structlog import get_logger
from passbook.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
from passbook.root.celery import CELERY_APP
LOGGER = get_logger()
VERSION_CACHE_KEY = "passbook_latest_version"
VERSION_CACHE_TIMEOUT = 2 * 60 * 60 # 2 hours
@CELERY_APP.task(bind=True, base=MonitoredTask)
def update_latest_version(self: MonitoredTask):
"""Update latest version info"""
try:
data = get(
"https://api.github.com/repos/beryju/passbook/releases/latest"
).json()
tag_name = data.get("tag_name")
cache.set(VERSION_CACHE_KEY, tag_name.split("/")[1], VERSION_CACHE_TIMEOUT)
self.set_status(
TaskResult(
TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"]
)
)
except (RequestException, IndexError) as exc:
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))

View File

@ -18,6 +18,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
@ -62,18 +63,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-applications pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Applications.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any application." %}
{% else %}
{% trans 'Currently no applications exist. Click the button below to create one.' %}
{% endif %}
</div>
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>

View File

@ -25,7 +25,7 @@
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:overview' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:overview' %}">
{% trans 'System Status' %}
{% trans 'Overview' %}
</a>
</li>
<li class="pf-c-nav__item">
@ -58,7 +58,7 @@
{% trans 'Property Mappings' %}
</a>
</li>
<li class="pf-c-nav__item pf-m-expandable pf-m-expanded">
<li class="pf-c-nav__item pf-m-expanded">
<a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Flows' %}
<span class="pf-c-nav__toggle">
<i class="fas fa-angle-right" aria-hidden="true"></i>
@ -99,7 +99,7 @@
</ul>
</section>
</li>
<li class="pf-c-nav__item pf-m-expandable pf-m-expanded">
<li class="pf-c-nav__item pf-m-expanded">
<a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Policies' %}
<span class="pf-c-nav__toggle">
<i class="fas fa-angle-right" aria-hidden="true"></i>
@ -146,6 +146,12 @@
{% trans 'Groups' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:tasks' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:tasks' %}">
{% trans 'System Tasks' %}
</a>
</li>
</ul>
</nav>
</div>

View File

@ -18,6 +18,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
@ -64,18 +65,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-key pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Certificates.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any certificates." %}
{% else %}
{% trans 'Currently no certificates exist. Click the button below to create one.' %}
{% endif %}
</div>
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>

View File

@ -18,6 +18,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
<a href="{% url 'passbook_admin:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-secondary" type="button">{% trans 'Import' %}</a>
@ -69,18 +70,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-process-automation pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Flows.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any flows." %}
{% else %}
{% trans 'Currently no flows exist. Click the button below to create one.' %}
{% endif %}
</div>
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
<a href="{% url 'passbook_admin:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Import' %}</a>

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