Compare commits

..

117 Commits

Author SHA1 Message Date
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
222 changed files with 4298 additions and 1675 deletions

View File

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

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.

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

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

View File

@ -18,11 +18,11 @@ jobs:
- name: Building Docker Image
run: docker build
--no-cache
-t beryju/passbook:0.10.4-stable
-t beryju/passbook:0.10.8-stable
-t beryju/passbook:latest
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook:0.10.4-stable
run: docker push beryju/passbook:0.10.8-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook:latest
build-proxy:
@ -48,11 +48,11 @@ jobs:
cd proxy
docker build \
--no-cache \
-t beryju/passbook-proxy:0.10.4-stable \
-t beryju/passbook-proxy:0.10.8-stable \
-t beryju/passbook-proxy:latest \
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-proxy:0.10.4-stable
run: docker push beryju/passbook-proxy:0.10.8-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-proxy:latest
build-static:
@ -77,11 +77,11 @@ jobs:
run: docker build
--no-cache
--network=$(docker network ls | grep github | awk '{print $1}')
-t beryju/passbook-static:0.10.4-stable
-t beryju/passbook-static:0.10.8-stable
-t beryju/passbook-static:latest
-f static.Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-static:0.10.4-stable
run: docker push beryju/passbook-static:0.10.8-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-static:latest
test-release:
@ -114,5 +114,5 @@ jobs:
SENTRY_PROJECT: passbook
SENTRY_URL: https://sentry.beryju.org
with:
tagName: 0.10.4-stable
tagName: 0.10.8-stable
environment: beryjuorg-prod

View File

@ -17,10 +17,10 @@ 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/ && \
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

@ -8,12 +8,12 @@ coverage:
lint-fix:
isort -rc .
black .
black passbook e2e lifecycle
lint:
pyright
bandit -r .
pylint passbook
pyright pyright e2e lifecycle
bandit -r passbook e2e lifecycle
pylint passbook e2e lifecycle
prospector
gen: coverage

View File

@ -17,7 +17,7 @@ django-otp = "*"
django-prometheus = "*"
django-recaptcha = "*"
django-redis = "*"
django-rest-framework = "*"
djangorestframework = "==3.11.1"
django-storages = "*"
djangorestframework-guardian = "*"
drf-yasg = "*"

291
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "80570636236962f4b934a884817292de9f7bb48520aa964afc2959b0f795fb57"
"sha256": "39e0a747699dc7e528a215395cc505b380e40e6bd0295fdf4c373a871a9bde96"
},
"pipfile-spec": 6,
"requires": {
@ -16,6 +16,24 @@
]
},
"default": {
"aiohttp": {
"hashes": [
"sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e",
"sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326",
"sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a",
"sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654",
"sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a",
"sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4",
"sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17",
"sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec",
"sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd",
"sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48",
"sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59",
"sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"
],
"markers": "python_version >= '3.6'",
"version": "==3.6.2"
},
"aioredis": {
"hashes": [
"sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a",
@ -25,10 +43,10 @@
},
"amqp": {
"hashes": [
"sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21",
"sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"
"sha256:9881f8e6fe23e3db9faa6cfd8c05390213e1d1b95c0162bc50552cad75bffa5f",
"sha256:a8fb8151eb9d12204c9f1784c0da920476077609fa0a70f2468001e3a4258484"
],
"version": "==2.6.1"
"version": "==5.0.1"
},
"asgiref": {
"hashes": [
@ -74,17 +92,18 @@
},
"boto3": {
"hashes": [
"sha256:44073b1b1823ffc9edcf9027afbca908dad6bd5000f512ca73f929f6a604ae24"
"sha256:348cddfd56be6c8b759544f99d20d633cc6a65d346651700ad8ac93a5214c032",
"sha256:c4ccd1f260660603f965bcc145de87e09dd1229040784fe119cd08caeb00dbe9"
],
"index": "pypi",
"version": "==1.15.1"
"version": "==1.15.8"
},
"botocore": {
"hashes": [
"sha256:6bdf60281c2e80360fe904851a1a07df3dcfe066fe88dc7fba2b5e626ac05c8c",
"sha256:d6bdf51c8880aa9974e6b61d2f7d9d1debe407287e2e9e60f36c789fe8ba6790"
"sha256:07b399997d8050d3ed1150d4d657b46558999f75246eb5b02cee78b9798b3bd5",
"sha256:53a778e6b715ad2ae39bf98e088962e8d524133fb458d83f080964254adc9885"
],
"version": "==1.18.1"
"version": "==1.18.8"
},
"cachetools": {
"hashes": [
@ -95,11 +114,11 @@
},
"celery": {
"hashes": [
"sha256:a92e1d56e650781fb747032a3997d16236d037c8199eacd5217d1a72893bca45",
"sha256:d220b13a8ed57c78149acf82c006785356071844afe0b27012a4991d44026f9f"
"sha256:313930fddde703d8e37029a304bf91429cd11aeef63c57de6daca9d958e1f255",
"sha256:72138dc3887f68dc58e1a2397e477256f80f1894c69fa4337f8ed70be460375b"
],
"index": "pypi",
"version": "==4.4.7"
"version": "==5.0.0"
},
"certifi": {
"hashes": [
@ -179,6 +198,19 @@
],
"version": "==7.1.2"
},
"click-didyoumean": {
"hashes": [
"sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb"
],
"version": "==0.0.3"
},
"click-repl": {
"hashes": [
"sha256:9c4c3d022789cae912aad8a3f5e1d7c2cdd016ee1225b5212ad3e8691563cda5",
"sha256:b9f29d52abc4d6059f8e276132a111ab8d94980afe6a5432b9d996544afa95d5"
],
"version": "==0.1.6"
},
"constantly": {
"hashes": [
"sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35",
@ -272,11 +304,11 @@
},
"django-filter": {
"hashes": [
"sha256:11e63dd759835d9ba7a763926ffb2662cf8a6dcb4c7971a95064de34dbc7e5af",
"sha256:616848eab6fc50193a1b3730140c49b60c57a3eda1f7fc57fa8505ac156c6c75"
"sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06",
"sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"
],
"index": "pypi",
"version": "==2.3.0"
"version": "==2.4.0"
},
"django-guardian": {
"hashes": [
@ -325,13 +357,6 @@
"index": "pypi",
"version": "==4.12.1"
},
"django-rest-framework": {
"hashes": [
"sha256:47a8f496fa69e3b6bd79f68dd7a1527d907d6b77f009e9db7cf9bb21cc565e4a"
],
"index": "pypi",
"version": "==0.1.0"
},
"django-storages": {
"hashes": [
"sha256:12de8fb2605b9b57bfaf54b075280d7cbb3b3ee1ca4bc9b9add147af87fe3a2c",
@ -345,6 +370,7 @@
"sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32",
"sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b"
],
"index": "pypi",
"version": "==3.11.1"
},
"djangorestframework-guardian": {
@ -386,10 +412,10 @@
},
"google-auth": {
"hashes": [
"sha256:7084c50c03f7a8a5696ef4500e65df0c525a0f6909f3c70b9ee65900a230c755",
"sha256:dcf86c5adc3a8a7659be190b12bb8912ae019cfd9ee2a571ea881e289fafbe39"
"sha256:a73e6fb6d232ed1293ef9a5301e6f8aada7880d19c65d7f63e130dc50ec05593",
"sha256:e86e72142d939a8d90a772947268aacc127ab7a1d1d6f3e0fecca7a8d74d8257"
],
"version": "==1.21.2"
"version": "==1.22.0"
},
"gunicorn": {
"hashes": [
@ -401,10 +427,10 @@
},
"h11": {
"hashes": [
"sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1",
"sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"
"sha256:311dc5478c2568cc07262e0381cdfc5b9c6ba19775905736c87e81ae6662b9fd",
"sha256:9eecfbafc980976dbff26a01dd3487644dd5d00f8038584451fc64a660f7c502"
],
"version": "==0.9.0"
"version": "==0.10.0"
},
"hiredis": {
"hashes": [
@ -457,24 +483,6 @@
],
"version": "==1.1.0"
},
"httptools": {
"hashes": [
"sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be",
"sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d",
"sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce",
"sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2",
"sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6",
"sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f",
"sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009",
"sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce",
"sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a",
"sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c",
"sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4",
"sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"
],
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'",
"version": "==0.1.1"
},
"hyperlink": {
"hashes": [
"sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af",
@ -533,10 +541,10 @@
},
"kombu": {
"hashes": [
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
"sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
"sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006",
"sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c"
],
"version": "==4.6.11"
"version": "==5.0.2"
},
"kubernetes": {
"hashes": [
@ -652,6 +660,28 @@
],
"version": "==1.0.0"
},
"multidict": {
"hashes": [
"sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a",
"sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000",
"sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2",
"sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507",
"sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5",
"sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7",
"sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d",
"sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463",
"sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19",
"sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3",
"sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b",
"sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c",
"sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87",
"sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7",
"sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430",
"sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255",
"sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"
],
"version": "==4.7.6"
},
"oauthlib": {
"hashes": [
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
@ -674,6 +704,13 @@
],
"version": "==0.8.0"
},
"prompt-toolkit": {
"hashes": [
"sha256:822f4605f28f7d2ba6b0b09a31e25e140871e96364d1d377667b547bb3bf4489",
"sha256:83074ee28ad4ba6af190593d4d4c607ff525272a504eb159199b6dd9f950c950"
],
"version": "==3.0.7"
},
"psycopg2-binary": {
"hashes": [
"sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
@ -748,20 +785,25 @@
"sha256:54bdedd28476dea8a3cd86cb67c0df1f0e3d71cae8022354b0f879c41a3d27b2",
"sha256:55eb61aca2c883db770999f50d091ff7c14016f2769ad7bca3d9b75d1d7c1b68",
"sha256:6276478ada411aca97c0d5104916354b3d740d368407912722bd4d11aa9ee4c2",
"sha256:663f8de2b3df2e744d6e1610506e0ea4e213bde906795953c1e82279c169f0a7",
"sha256:67dcad1b8b201308586a8ca2ffe89df1e4f731d5a4cdd0610cc4ea790351c739",
"sha256:709b9f144d23e290b9863121d1ace14a72e01f66ea9c903fbdc690520dfdfcf0",
"sha256:8063a712fba642f78d3c506b0896846601b6de7f5c3d534e388ad0cc07f5a149",
"sha256:80d57177a0b7c14d4594c62bbb47fe2f6309ad3b0a34348a291d570925c97a82",
"sha256:87006cf0d81505408f1ae4f55cf8a5d95a8e029a4793360720ae17c6500f7ecc",
"sha256:9f62d21bc693f3d7d444f17ed2ad7a913b4c37c15cd807895d013c39c0517dfd",
"sha256:a207231a52426de3ff20f5608f0687261a3329d97a036c51f7d4c606a6f30c23",
"sha256:abc2e126c9490e58a36a0f83516479e781d83adfb134576a5cbe5c6af2a3e93c",
"sha256:b56638d58a3a4be13229c6a815cd448f9e3ce40c00880a5398471b42ee86f50e",
"sha256:bcd5b8416e73e4b0d48afba3704d8c826414764dafaed7a1a93c442188d90ccc",
"sha256:bec2bcdf7c9ce7f04d718e51887f3b05dc5c1cfaf5d2c2e9065ecddd1b2f6c9a",
"sha256:c8bf40cf6e281a4378e25846924327e728a887e8bf0ee83b2604a0f4b61692e8",
"sha256:cecbf67e81d6144a50dc615629772859463b2e4f815d0c082fa421db362f040e",
"sha256:d8074c8448cfd0705dfa71ca333277fce9786d0b9cac75d120545de6253f996a",
"sha256:dd302b6ae3965afeb5ef1b0d92486f986c0e65183cd7835973f0b593800590e6",
"sha256:de6e1cd75677423ff64712c337521e62e3a7a4fc84caabbd93207752e831a85a",
"sha256:ef39c98d9b8c0736d91937d193653e47c3b19ddf4fc3bccdc5e09aaa4b0c5d21",
"sha256:f2e045224074d5664dc9cbabbf4f4d4d46f1ee90f24780e3a9a668fd096ff17f",
"sha256:f521178e5a991ffd04182ed08f552daca1affcb826aeda0e1945cd989a9d4345",
"sha256:f78a68c2c820e4731e510a2df3eef0322f24fde1781ced970bf497b6c7d92982",
"sha256:fbe65d5cfe04ff2f7684160d50f5118bdefb01e3af4718eeb618bfed40f19d94"
@ -778,12 +820,14 @@
"sha256:2275a663c9e744ee4eace816ef2d446b3060554c5773a92fbc79b05bf47debda",
"sha256:2710fc8d83b3352b370db932b3710033b9d630b970ff5aaa3e7458b5336e3b32",
"sha256:35b9c9177a9fe7288b19dd41554c9c8ca1063deb426dd5a02e7e2a7416b6bd11",
"sha256:3b23d63030819b7d9ac7db9360305fd1241e6870ca5b7e8d59fee4db4674a490",
"sha256:3caa32cf807422adf33c10c88c22e9e2e08b9d9d042f12e1e25fe23113dd618f",
"sha256:48cc2cfc251f04a6142badeb666d1ff49ca6fdfc303fd72579f62b768aaa52b9",
"sha256:4ae6379350a09339109e9b6f419bb2c3f03d3e441f4b0f5b8ca699d47cc9ff7e",
"sha256:4e0b27697fa1621c6d3d3b4edeec723c2e841285de6a8d378c1962da77b349be",
"sha256:58e19560814dabf5d788b95a13f6b98279cf41a49b1e49ee6cf6c79a57adb4c9",
"sha256:8044eae59301dd392fbb4a7c5d64e1aea8ef0be2540549807ecbe703d6233d68",
"sha256:85c108b42e47d4073344ff61d4e019f1d95bb7725ca0fe87d0a2deb237c10e49",
"sha256:89be1bf55e50116fe7e493a7c0c483099770dd7f81b87ac8d04a43b1a203e259",
"sha256:8fcdda24dddf47f716400d54fc7f75cadaaba1dd47cc127e59d752c9c0fc3c48",
"sha256:914fbb18e29c54585e6aa39d300385f90d0fa3b3cc02ed829b08f95c1acf60c2",
@ -793,13 +837,16 @@
"sha256:a2ee8ba99d33e1a434fcd27d7d0aa7964163efeee0730fe2efc9d60edae1fc71",
"sha256:b2d756620078570d3f940c84bc94dd30aa362b795cce8b2723300a8800b87f1c",
"sha256:c0d085c8187a1e4d3402f626c9e438b5861151ab132d8761d9c5ce6491a87761",
"sha256:c315262e26d54a9684e323e37ac9254f481d57fcc4fd94002992460898ef5c04",
"sha256:c990f2c58f7c67688e9e86e6557ed05952669ff6f1343e77b459007d85f7df00",
"sha256:ccbbec59bf4b74226170c54476da5780c9176bae084878fc94d9a2c841218e34",
"sha256:dc2bed32c7b138f1331794e454a953360c8cedf3ee62ae31f063822da6007489",
"sha256:ddb1ae2891c8cb83a25da87a3e00111a9654fc5f0b70f18879c41aece45d6182",
"sha256:e070a1f91202ed34c396be5ea842b886f6fa2b90d2db437dc9fb35a26c80c060",
"sha256:e42860fbe1292668b682f6dabd225fbe2a7a4fa1632f0c39881c019e93dea594",
"sha256:e4e1c486bf226822c8dceac81d0ec59c0a2399dbd1b9e04f03c3efa3605db677",
"sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46",
"sha256:f5bd6891380e0fb5467251daf22525644fdf6afd9ae8bc2fe065c78ea1882e0d",
"sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb"
],
"version": "==3.9.8"
@ -951,11 +998,11 @@
},
"sentry-sdk": {
"hashes": [
"sha256:1a086486ff9da15791f294f6e9915eb3747d161ef64dee2d038a4d0b4a369b24",
"sha256:45486deb031cea6bbb25a540d7adb4dd48cd8a1cc31e6a5ce9fb4f792a572e9a"
"sha256:1d91a0059d2d8bb980bec169578035c2f2d4b93cd8a4fb5b85c81904d33e221a",
"sha256:6222cf623e404c3e62b8e0e81c6db866ac2d12a663b7c1f7963350e3f397522a"
],
"index": "pypi",
"version": "==0.17.6"
"version": "==0.18.0"
},
"service-identity": {
"hashes": [
@ -1062,33 +1109,25 @@
},
"uvicorn": {
"hashes": [
"sha256:46a83e371f37ea7ff29577d00015f02c942410288fb57def6440f2653fff1d26",
"sha256:4b70ddb4c1946e39db9f3082d53e323dfd50634b95fd83625d778729ef1730ef"
"sha256:9a8f3501d977dedf77a540a0ec3cfadf409fe48eafca2c100d45d843ac62bc7b",
"sha256:fbe9d1b764bc1f4599e1f150a0974feea0fd6380bec889c0d907ebd0a2e896a7"
],
"index": "pypi",
"version": "==0.11.8"
},
"uvloop": {
"hashes": [
"sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd",
"sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e",
"sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09",
"sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726",
"sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891",
"sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7",
"sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5",
"sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95",
"sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"
],
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'",
"version": "==0.14.0"
"version": "==0.12.0"
},
"vine": {
"hashes": [
"sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87",
"sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
],
"version": "==1.3.0"
"version": "==5.0.0"
},
"wcwidth": {
"hashes": [
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
"sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
],
"version": "==0.2.5"
},
"websocket-client": {
"hashes": [
@ -1097,32 +1136,27 @@
],
"version": "==0.57.0"
},
"websockets": {
"yarl": {
"hashes": [
"sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5",
"sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5",
"sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308",
"sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb",
"sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a",
"sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c",
"sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170",
"sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422",
"sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8",
"sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485",
"sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f",
"sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8",
"sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc",
"sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779",
"sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989",
"sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1",
"sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092",
"sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824",
"sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d",
"sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55",
"sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36",
"sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"
"sha256:04a54f126a0732af75e5edc9addeaa2113e2ca7c6fce8974a63549a70a25e50e",
"sha256:3cc860d72ed989f3b1f3abbd6ecf38e412de722fb38b8f1b1a086315cf0d69c5",
"sha256:5d84cc36981eb5a8533be79d6c43454c8e6a39ee3118ceaadbd3c029ab2ee580",
"sha256:5e447e7f3780f44f890360ea973418025e8c0cdcd7d6a1b221d952600fd945dc",
"sha256:61d3ea3c175fe45f1498af868879c6ffeb989d4143ac542163c45538ba5ec21b",
"sha256:67c5ea0970da882eaf9efcf65b66792557c526f8e55f752194eff8ec722c75c2",
"sha256:6f6898429ec3c4cfbef12907047136fd7b9e81a6ee9f105b45505e633427330a",
"sha256:7ce35944e8e61927a8f4eb78f5bc5d1e6da6d40eadd77e3f79d4e9399e263921",
"sha256:b7c199d2cbaf892ba0f91ed36d12ff41ecd0dde46cbf64ff4bfe997a3ebc925e",
"sha256:c15d71a640fb1f8e98a1423f9c64d7f1f6a3a168f803042eaf3a5b5022fde0c1",
"sha256:c22607421f49c0cb6ff3ed593a49b6a99c6ffdeaaa6c944cdda83c2393c8864d",
"sha256:c604998ab8115db802cc55cb1b91619b2831a6128a62ca7eea577fc8ea4d3131",
"sha256:d088ea9319e49273f25b1c96a3763bf19a882cff774d1792ae6fba34bd40550a",
"sha256:db9eb8307219d7e09b33bcb43287222ef35cbcf1586ba9472b0a4b833666ada1",
"sha256:e31fef4e7b68184545c3d68baec7074532e077bd1906b040ecfba659737df188",
"sha256:e32f0fb443afcfe7f01f95172b66f279938fbc6bdaebe294b0ff6747fb6db020",
"sha256:fcbe419805c9b20db9a51d33b942feddbf6e7fb468cb20686fd7089d4164c12a"
],
"version": "==8.1"
"version": "==1.6.0"
},
"zope.interface": {
"hashes": [
@ -1316,11 +1350,11 @@
},
"django-debug-toolbar": {
"hashes": [
"sha256:eabbefe89881bbe4ca7c980ff102e3c35c8e8ad6eb725041f538988f2f39a943",
"sha256:ff94725e7aae74b133d0599b9bf89bd4eb8f5d2c964106e61d11750228c8774c"
"sha256:a1ce0665f7ef47d27b8df4b5d1058748e1f08500a01421a30d35164f38aaaf4c",
"sha256:c97921a9cd421d392e7860dc4b464db8e06c8628df4dc58fedab012888c293c6"
],
"index": "pypi",
"version": "==2.2"
"version": "==3.1.1"
},
"docker": {
"hashes": [
@ -1419,13 +1453,6 @@
],
"version": "==0.6.1"
},
"more-itertools": {
"hashes": [
"sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20",
"sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"
],
"version": "==8.5.0"
},
"packaging": {
"hashes": [
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
@ -1541,11 +1568,11 @@
},
"pytest": {
"hashes": [
"sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40",
"sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043"
"sha256:1cd09785c0a50f9af72220dd12aa78cfa49cbffc356c61eab009ca189e018a33",
"sha256:d010e24666435b39a4cf48740b039885642b6c273a3f77be3e7e03554d2806b7"
],
"index": "pypi",
"version": "==6.0.2"
"version": "==6.1.0"
},
"pytest-django": {
"hashes": [
@ -1581,29 +1608,29 @@
},
"regex": {
"hashes": [
"sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204",
"sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162",
"sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f",
"sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb",
"sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6",
"sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7",
"sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88",
"sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99",
"sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644",
"sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a",
"sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840",
"sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067",
"sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd",
"sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4",
"sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e",
"sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89",
"sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e",
"sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc",
"sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf",
"sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341",
"sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"
"sha256:088afc8c63e7bd187a3c70a94b9e50ab3f17e1d3f52a32750b5b77dbe99ef5ef",
"sha256:1fe0a41437bbd06063aa184c34804efa886bcc128222e9916310c92cd54c3b4c",
"sha256:41bb65f54bba392643557e617316d0d899ed5b4946dccee1cb6696152b29844b",
"sha256:4318d56bccfe7d43e5addb272406ade7a2274da4b70eb15922a071c58ab0108c",
"sha256:4707f3695b34335afdfb09be3802c87fa0bc27030471dbc082f815f23688bc63",
"sha256:5533a959a1748a5c042a6da71fe9267a908e21eded7a4f373efd23a2cbdb0ecc",
"sha256:5f18875ac23d9aa2f060838e8b79093e8bb2313dbaaa9f54c6d8e52a5df097be",
"sha256:60b0e9e6dc45683e569ec37c55ac20c582973841927a85f2d8a7d20ee80216ab",
"sha256:84e9407db1b2eb368b7ecc283121b5e592c9aaedbe8c78b1a2f1102eb2e21d19",
"sha256:8d69cef61fa50c8133382e61fd97439de1ae623fe943578e477e76a9d9471637",
"sha256:9a02d0ae31d35e1ec12a4ea4d4cca990800f66a917d0fb997b20fbc13f5321fc",
"sha256:9bc13e0d20b97ffb07821aa3e113f9998e84994fe4d159ffa3d3a9d1b805043b",
"sha256:a6f32aea4260dfe0e55dc9733ea162ea38f0ea86aa7d0f77b15beac5bf7b369d",
"sha256:ae91972f8ac958039920ef6e8769277c084971a142ce2b660691793ae44aae6b",
"sha256:c570f6fa14b9c4c8a4924aaad354652366577b4f98213cf76305067144f7b100",
"sha256:d23a18037313714fb3bb5a94434d3151ee4300bae631894b1ac08111abeaa4a3",
"sha256:eaf548d117b6737df379fdd53bdde4f08870e66d7ea653e230477f071f861121",
"sha256:ebbe29186a3d9b0c591e71b7393f1ae08c83cb2d8e517d2a822b8f7ec99dfd8b",
"sha256:eda4771e0ace7f67f58bc5b560e27fb20f32a148cbc993b0c3835970935c2707",
"sha256:f1b3afc574a3db3b25c89161059d857bd4909a1269b0b3cb3c904677c8c4a3f7",
"sha256:f2388013e68e750eaa16ccbea62d4130180c26abb1d8e5d584b9baf69672b30f"
],
"version": "==2020.7.14"
"version": "==2020.9.27"
},
"requests": {
"hashes": [

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
@ -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'
@ -140,6 +179,9 @@ stages:
inputs:
script: |
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
@ -182,7 +224,7 @@ stages:
displayName: Run full test suite
inputs:
script: |
pipenv run coverage run ./manage.py test e2e -v 3
pipenv run coverage run ./manage.py test e2e -v 3 --failfast
- task: CmdLine@2
condition: always()
displayName: Cleanup
@ -265,7 +307,7 @@ stages:
repository: 'beryju/passbook'
command: 'buildAndPush'
Dockerfile: 'Dockerfile'
tags: 'gh-$(Build.SourceBranchName)'
tags: "gh-${{ variables.branchName }}"
- job: build_static
pool:
vmImage: 'ubuntu-latest'
@ -282,14 +324,14 @@ 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)'
tags: "gh-${{ variables.branchName }}"
- stage: Deploy
jobs:
- job: deploy_dev

View File

@ -23,13 +23,12 @@ services:
labels:
- traefik.enable=false
server:
image: beryju/passbook:${PASSBOOK_TAG:-0.10.4-stable}
image: beryju/passbook:${PASSBOOK_TAG:-0.10.8-stable}
command: server
environment:
PASSBOOK_REDIS__HOST: redis
PASSBOOK_POSTGRESQL__HOST: postgresql
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
PASSBOOK_LOG_LEVEL: debug
ports:
- 8000
networks:
@ -41,7 +40,7 @@ services:
env_file:
- .env
worker:
image: beryju/passbook:${PASSBOOK_TAG:-0.10.4-stable}
image: beryju/passbook:${PASSBOOK_TAG:-0.10.8-stable}
command: worker
networks:
- internal
@ -51,11 +50,10 @@ services:
PASSBOOK_REDIS__HOST: redis
PASSBOOK_POSTGRESQL__HOST: postgresql
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
PASSBOOK_LOG_LEVEL: debug
env_file:
- .env
static:
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.4-stable}
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.8-stable}
networks:
- internal
labels:

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",
@ -118,9 +108,6 @@
"fields": [
"7db91ee8-4290-4e08-8d39-63f132402515",
"d30b5eb4-7787-4072-b1ba-65b46e928920"
],
"validation_policies": [
"cd042fc6-cc92-4b98-b7e6-f4729df798d8"
]
}
},

View File

@ -13,7 +13,7 @@ Download the latest `docker-compose.yml` from [here](https://raw.githubuserconte
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING__ENABLED=true >> .env`
To optionally deploy a different version run `echo PASSBOOK_TAG=0.10.4-stable >> .env`
To optionally deploy a different version run `echo PASSBOOK_TAG=0.10.8-stable >> .env`
If this is a fresh passbook install run the following commands to generate a password:
@ -39,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

@ -11,7 +11,7 @@ This installation automatically applies database migrations on startup. After th
image:
name: beryju/passbook
name_static: beryju/passbook-static
tag: 0.10.4-stable
tag: 0.10.8-stable
nameOverride: ""

View File

@ -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

@ -19,6 +19,7 @@ Create an application in passbook and note the slug, as this will be used later.
- 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: {

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, called for example "Tautulli Users". 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 than 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 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, just like descried [here](../sonarr/index.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 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

@ -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-20200719
image: selenium/standalone-chrome-debug:3.141
volumes:
- /dev/shm:/dev/shm
network_mode: host

View File

@ -10,7 +10,6 @@ from selenium.webdriver.support import expected_conditions as ec
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
@ -59,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])
@ -112,8 +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,
"foo",
self.driver.find_element(By.ID, "user-settings").text, "foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
@ -152,16 +143,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,16 +204,11 @@ class TestFlowsEnroll(SeleniumTestCase):
self.driver.switch_to.window(self.driver.window_handles[0])
# We're now 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.find_element(By.ID, "user-settings").click()
self.assertEqual(
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
"foo",
self.driver.find_element(By.ID, "user-settings").text, "foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"

View File

@ -21,6 +21,5 @@ 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,
USER().username,
self.driver.find_element(By.ID, "user-settings").text, USER().username,
)

138
e2e/test_flows_otp.py Normal file
View File

@ -0,0 +1,138 @@
"""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

@ -20,12 +20,12 @@ 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 = generate_client_secret()
@ -38,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)

View File

@ -1,5 +1,6 @@
"""test OAuth Provider flow"""
from sys import platform
from time import sleep
from typing import Any, Dict, Optional
from unittest.case import skipUnless
@ -88,7 +89,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,
@ -139,28 +140,19 @@ 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(
app.name,
self.driver.find_element(
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
).text,
sleep(1)
self.assertEqual(
app.name, 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,
)
self.driver.find_element(
By.CSS_SELECTOR,
(
"form[action='/flows/b/default-provider-authorization-explicit-consent/'] "
"[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,

View File

@ -0,0 +1,364 @@
"""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,9 +1,11 @@
"""test OAuth2 OpenID Provider flow"""
from json import loads
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
@ -33,7 +35,6 @@ from passbook.providers.oauth2.models import (
)
LOGGER = get_logger()
APPLICATION_SLUG = "grafana"
@skipUnless(platform.startswith("linux"), "requires local docker")
@ -43,42 +44,37 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
def setUp(self):
self.client_id = generate_client_id()
self.client_secret = generate_client_secret()
self.application_slug = "test"
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"],
def setup_client(self) -> Container:
"""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="beryju/oidc-test-client",
detach=True,
network_mode="host",
auto_remove=True,
healthcheck=Healthcheck(
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_AUTH_SIGNOUT_REDIRECT_URL": (
self.url(
"passbook_providers_oauth2:end-session",
application_slug=APPLICATION_SLUG,
)
),
"GF_LOG_LEVEL": "debug",
environment={
"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:
container.reload()
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
if status == "healthy":
return container
LOGGER.info("Container failed healthcheck")
sleep(1)
def test_redirect_uri_error(self):
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
@ -88,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,
)
@ -104,11 +100,12 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
)
provider.save()
Application.objects.create(
name="Grafana", slug=APPLICATION_SLUG, 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)
@ -128,12 +125,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,
)
@ -144,105 +141,29 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
)
provider.save()
Application.objects.create(
name="Grafana", slug=APPLICATION_SLUG, 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,
)
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.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre")))
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
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.driver.find_element(By.CSS_SELECTOR, "[href='/logout']").click()
self.wait_for_url(
self.url(
"passbook_providers_oauth2:end-session",
application_slug=APPLICATION_SLUG,
)
)
self.driver.find_element(By.ID, "logout").click()
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)"""
@ -252,14 +173,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(
@ -268,22 +189,20 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
)
provider.save()
app = Application.objects.create(
name="Grafana", slug=APPLICATION_SLUG, 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(
app.name,
self.driver.find_element(
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
).text,
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]"))
@ -291,34 +210,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)"""
@ -328,14 +230,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(
@ -344,15 +246,17 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
)
provider.save()
app = Application.objects.create(
name="Grafana", slug=APPLICATION_SLUG, 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)

View File

@ -4,12 +4,14 @@ 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, OutpostDeploymentType, OutpostType
@ -37,6 +39,7 @@ class TestProviderProxy(SeleniumTestCase):
def start_proxy(self, outpost: Outpost) -> Container:
"""Start proxy container based on outpost created"""
client: DockerClient = from_env()
client.images.pull("beryju/oidc-test-client")
container = client.containers.run(
image="beryju/passbook-proxy:latest",
detach=True,
@ -94,3 +97,67 @@ class TestProviderProxy(SeleniumTestCase):
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"""
proxy_container: Container
def tearDown(self) -> None:
self.proxy_container.kill()
super().tearDown()
def start_proxy(self, outpost: Outpost) -> Container:
"""Start proxy container based on outpost created"""
client: DockerClient = from_env()
client.images.pull("beryju/oidc-test-client")
container = client.containers.run(
image="beryju/passbook-proxy:latest",
detach=True,
network_mode="host",
auto_remove=True,
environment={
"PASSBOOK_HOST": self.live_server_url,
"PASSBOOK_TOKEN": outpost.token.token_uuid.hex,
},
)
return container
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.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 outpost.deployment_health:
break
healthcheck_retries += 1
sleep(0.5)
self.assertIsNotNone(outpost.deployment_health)
self.assertEqual(outpost.deployment_version.get("version"), __version__)

View File

@ -1,4 +1,5 @@
"""test SAML Provider flow"""
from json import loads
from sys import platform
from time import sleep
from unittest.case import skipUnless
@ -35,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,
@ -92,10 +94,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)"""
@ -124,19 +130,20 @@ 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(
app.name,
self.driver.find_element(
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
).text,
self.assertEqual(
app.name, 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)"""
@ -170,11 +177,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)"""

View File

@ -16,16 +16,18 @@ 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.providers.oauth2.generators import (
generate_client_id,
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()
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestSourceOAuth(SeleniumTestCase):
class TestSourceOAuth2(SeleniumTestCase):
"""test OAuth Source flow"""
container: Container
@ -91,14 +93,14 @@ class TestSourceOAuth(SeleniumTestCase):
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
OAuthSource.objects.create(
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=TOKEN_URL,
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,
@ -138,13 +140,11 @@ class TestSourceOAuth(SeleniumTestCase):
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.until(ec.presence_of_element_located((By.ID, "user-settings")))
self.driver.get(self.url("passbook_core:user-settings"))
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.driver.find_element(By.ID, "user-settings").text, "foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
@ -197,7 +197,7 @@ class TestSourceOAuth(SeleniumTestCase):
"""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.driver.find_element(By.ID, "logout").click()
self.wait.until(
ec.presence_of_element_located(
@ -221,14 +221,11 @@ class TestSourceOAuth(SeleniumTestCase):
)
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.until(ec.presence_of_element_located((By.ID, "user-settings")))
self.driver.get(self.url("passbook_core:user-settings"))
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.driver.find_element(By.ID, "user-settings").text, "foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
@ -240,3 +237,97 @@ class TestSourceOAuth(SeleniumTestCase):
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
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

@ -130,12 +130,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")))
@ -183,12 +179,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")))
@ -234,12 +226,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,5 +1,4 @@
"""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
@ -23,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"""
@ -40,8 +38,8 @@ class SeleniumTestCase(StaticLiveServerTestCase):
makedirs("selenium_screenshots/", exist_ok=True)
self.driver = self._get_driver()
self.driver.maximize_window()
self.driver.implicitly_wait(10)
self.wait = WebDriverWait(self.driver, 30)
self.driver.implicitly_wait(30)
self.wait = WebDriverWait(self.driver, 60)
self.apply_default_data()
self.logger = get_logger()
if specs := self.get_container_specs():
@ -49,6 +47,7 @@ class SeleniumTestCase(StaticLiveServerTestCase):
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

View File

@ -1,8 +1,8 @@
apiVersion: v2
appVersion: "0.10.4-stable"
appVersion: "0.10.8-stable"
description: A Helm chart for passbook.
name: passbook
version: "0.10.4-stable"
version: "0.10.8-stable"
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
dependencies:
- name: postgresql

View File

@ -4,7 +4,7 @@
image:
name: beryju/passbook
name_static: beryju/passbook-static
tag: 0.10.4-stable
tag: 0.10.8-stable
nameOverride: ""

View File

@ -4,7 +4,7 @@ printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap",
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 -Q passbook,passbook_scheduled
celery -A passbook.root.celery worker --autoscale 10,3 -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

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

@ -56,6 +56,7 @@ nav:
- 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
- Upgrading:
- to 0.9: upgrading/to-0.9.md
- to 0.10: upgrading/to-0.10.md

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = "0.10.4-stable"
__version__ = "0.10.8-stable"

View File

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

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

@ -0,0 +1,23 @@
"""passbook admin tasks"""
from django.core.cache import cache
from requests import RequestException, get
from structlog import get_logger
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()
def update_latest_version():
"""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)
except (RequestException, IndexError):
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)

View File

@ -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>

View File

@ -5,18 +5,6 @@
{% load passbook_utils %}
{% load admin_reflection %}
{% block head %}
{{ block.super }}
<style>
.pf-m-success {
color: var(--pf-global--success-color--100);
}
.pf-m-danger {
color: var(--pf-global--danger-color--100);
}
</style>
{% endblock %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">

View File

@ -9,24 +9,30 @@ from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
UserPaginateListMixin,
)
from passbook.core.forms.applications import ApplicationForm
from passbook.core.models import Application
from passbook.lib.views import CreateAssignPermView
class ApplicationListView(LoginRequiredMixin, PermissionListMixin, ListView):
class ApplicationListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
):
"""Show list of all applications"""
model = Application
permission_required = "passbook_core.view_application"
ordering = "name"
paginate_by = 40
template_name = "administration/application/list.html"
class ApplicationCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
@ -43,7 +49,11 @@ class ApplicationCreateView(
class ApplicationUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
UpdateView,
):
"""Update application"""

View File

@ -9,24 +9,30 @@ from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
UserPaginateListMixin,
)
from passbook.crypto.forms import CertificateKeyPairForm
from passbook.crypto.models import CertificateKeyPair
from passbook.lib.views import CreateAssignPermView
class CertificateKeyPairListView(LoginRequiredMixin, PermissionListMixin, ListView):
class CertificateKeyPairListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
):
"""Show list of all keypairs"""
model = CertificateKeyPair
permission_required = "passbook_crypto.view_certificatekeypair"
ordering = "name"
paginate_by = 40
template_name = "administration/certificatekeypair/list.html"
class CertificateKeyPairCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
@ -43,7 +49,11 @@ class CertificateKeyPairCreateView(
class CertificateKeyPairUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
UpdateView,
):
"""Update certificatekeypair"""

View File

@ -11,7 +11,11 @@ from django.utils.translation import gettext as _
from django.views.generic import DetailView, FormView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
UserPaginateListMixin,
)
from passbook.flows.forms import FlowForm, FlowImportForm
from passbook.flows.models import Flow
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
@ -23,18 +27,20 @@ from passbook.lib.utils.urls import redirect_with_qs
from passbook.lib.views import CreateAssignPermView
class FlowListView(LoginRequiredMixin, PermissionListMixin, ListView):
class FlowListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
):
"""Show list of all flows"""
model = Flow
permission_required = "passbook_flows.view_flow"
ordering = "name"
paginate_by = 40
template_name = "administration/flow/list.html"
class FlowCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
@ -51,7 +57,11 @@ class FlowCreateView(
class FlowUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
UpdateView,
):
"""Update flow"""

View File

@ -9,24 +9,30 @@ from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
UserPaginateListMixin,
)
from passbook.core.forms.groups import GroupForm
from passbook.core.models import Group
from passbook.lib.views import CreateAssignPermView
class GroupListView(LoginRequiredMixin, PermissionListMixin, ListView):
class GroupListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
):
"""Show list of all groups"""
model = Group
permission_required = "passbook_core.view_group"
ordering = "name"
paginate_by = 40
template_name = "administration/group/list.html"
class GroupCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
@ -43,7 +49,11 @@ class GroupCreateView(
class GroupUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
UpdateView,
):
"""Update group"""

View File

@ -12,24 +12,30 @@ from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
UserPaginateListMixin,
)
from passbook.lib.views import CreateAssignPermView
from passbook.outposts.forms import OutpostForm
from passbook.outposts.models import Outpost, OutpostConfig
class OutpostListView(LoginRequiredMixin, PermissionListMixin, ListView):
class OutpostListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
):
"""Show list of all outposts"""
model = Outpost
permission_required = "passbook_outposts.view_outpost"
ordering = "name"
paginate_by = 40
template_name = "administration/outpost/list.html"
class OutpostCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
@ -53,7 +59,11 @@ class OutpostCreateView(
class OutpostUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
UpdateView,
):
"""Update outpost"""

View File

@ -5,32 +5,16 @@ from django.core.cache import cache
from django.shortcuts import redirect, reverse
from django.views.generic import TemplateView
from packaging.version import LegacyVersion, Version, parse
from requests import RequestException, get
from passbook import __version__
from passbook.admin.mixins import AdminRequiredMixin
from passbook.admin.tasks import VERSION_CACHE_KEY, update_latest_version
from passbook.core.models import Application, Provider, Source, User
from passbook.flows.models import Flow, Stage
from passbook.policies.models import Policy
from passbook.root.celery import CELERY_APP
from passbook.stages.invitation.models import Invitation
VERSION_CACHE_KEY = "passbook_latest_version"
def latest_version() -> Union[LegacyVersion, Version]:
"""Get latest release from GitHub, cached"""
if not cache.get(VERSION_CACHE_KEY):
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], 30)
except (RequestException, IndexError):
cache.set(VERSION_CACHE_KEY, "0.0.0", 30)
return parse(cache.get(VERSION_CACHE_KEY))
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
"""Overview View"""
@ -44,6 +28,14 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
return redirect(reverse("passbook_flows:default-authentication"))
return self.get(*args, **kwargs)
def get_latest_version(self) -> Union[LegacyVersion, Version]:
"""Get latest version from cache"""
version_in_cache = cache.get(VERSION_CACHE_KEY)
if not version_in_cache:
update_latest_version.delay()
return parse(__version__)
return parse(version_in_cache)
def get_context_data(self, **kwargs):
kwargs["application_count"] = len(Application.objects.all())
kwargs["policy_count"] = len(Policy.objects.all())
@ -54,7 +46,7 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
kwargs["flow_count"] = len(Flow.objects.all())
kwargs["invitation_count"] = len(Invitation.objects.all())
kwargs["version"] = parse(__version__)
kwargs["version_latest"] = latest_version()
kwargs["version_latest"] = self.get_latest_version()
kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
kwargs["providers_without_application"] = Provider.objects.filter(
application=None

View File

@ -17,27 +17,31 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.forms.policies import PolicyTestForm
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
UserPaginateListMixin,
)
from passbook.policies.models import Policy, PolicyBinding
from passbook.policies.process import PolicyProcess, PolicyRequest
class PolicyListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
class PolicyListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
):
"""Show list of all policies"""
model = Policy
permission_required = "passbook_policies.view_policy"
paginate_by = 10
ordering = "name"
template_name = "administration/policy/list.html"
class PolicyCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
InheritanceCreateView,
@ -54,6 +58,7 @@ class PolicyCreateView(
class PolicyUpdateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,

View File

@ -9,19 +9,25 @@ from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from guardian.shortcuts import get_objects_for_user
from passbook.admin.views.utils import DeleteMessageView
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
UserPaginateListMixin,
)
from passbook.lib.views import CreateAssignPermView
from passbook.policies.forms import PolicyBindingForm
from passbook.policies.models import PolicyBinding, PolicyBindingModel
from passbook.policies.models import PolicyBinding
class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
class PolicyBindingListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
):
"""Show list of all policies"""
model = PolicyBinding
permission_required = "passbook_policies.view_policybinding"
paginate_by = 10
ordering = ["order", "target"]
template_name = "administration/policy_binding/list.html"
@ -29,18 +35,24 @@ class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
# Since `select_subclasses` does not work with a foreign key, we have to do two queries here
# First, get all pbm objects that have bindings attached
objects = (
PolicyBindingModel.objects.filter(policies__isnull=False)
get_objects_for_user(
self.request.user, "passbook_policies.view_policybindingmodel"
)
.filter(policies__isnull=False)
.select_subclasses()
.select_related()
.order_by("pk")
)
for pbm in objects:
pbm.bindings = PolicyBinding.objects.filter(target__pk=pbm.pbm_uuid)
pbm.bindings = get_objects_for_user(
self.request.user, self.permission_required
).filter(target__pk=pbm.pbm_uuid)
return objects
class PolicyBindingCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
@ -57,7 +69,11 @@ class PolicyBindingCreateView(
class PolicyBindingUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
UpdateView,
):
"""Update policybinding"""

View File

@ -9,16 +9,18 @@ from django.utils.translation import gettext as _
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
UserPaginateListMixin,
)
from passbook.core.models import PropertyMapping
class PropertyMappingListView(
LoginRequiredMixin, PermissionListMixin, InheritanceListView
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
):
"""Show list of all property_mappings"""
@ -26,11 +28,11 @@ class PropertyMappingListView(
permission_required = "passbook_core.view_propertymapping"
template_name = "administration/property_mapping/list.html"
ordering = "name"
paginate_by = 40
class PropertyMappingCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
InheritanceCreateView,
@ -47,6 +49,7 @@ class PropertyMappingCreateView(
class PropertyMappingUpdateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,

View File

@ -9,26 +9,30 @@ from django.utils.translation import gettext as _
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
UserPaginateListMixin,
)
from passbook.core.models import Provider
class ProviderListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
class ProviderListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
):
"""Show list of all providers"""
model = Provider
permission_required = "passbook_core.add_provider"
template_name = "administration/provider/list.html"
paginate_by = 10
ordering = "id"
class ProviderCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
InheritanceCreateView,
@ -45,6 +49,7 @@ class ProviderCreateView(
class ProviderUpdateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,

View File

@ -9,26 +9,30 @@ from django.utils.translation import gettext as _
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
UserPaginateListMixin,
)
from passbook.core.models import Source
class SourceListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
class SourceListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
):
"""Show list of all sources"""
model = Source
permission_required = "passbook_core.view_source"
ordering = "name"
paginate_by = 40
template_name = "administration/source/list.html"
class SourceCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
InheritanceCreateView,
@ -45,6 +49,7 @@ class SourceCreateView(
class SourceUpdateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,

View File

@ -9,26 +9,30 @@ from django.utils.translation import gettext as _
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
UserPaginateListMixin,
)
from passbook.flows.models import Stage
class StageListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
class StageListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
):
"""Show list of all stages"""
model = Stage
template_name = "administration/stage/list.html"
permission_required = "passbook_flows.view_stage"
ordering = "name"
paginate_by = 40
class StageCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
InheritanceCreateView,
@ -45,6 +49,7 @@ class StageCreateView(
class StageUpdateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,

View File

@ -9,24 +9,30 @@ from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
UserPaginateListMixin,
)
from passbook.flows.forms import FlowStageBindingForm
from passbook.flows.models import FlowStageBinding
from passbook.lib.views import CreateAssignPermView
class StageBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
class StageBindingListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
):
"""Show list of all flows"""
model = FlowStageBinding
permission_required = "passbook_flows.view_flowstagebinding"
paginate_by = 10
ordering = ["target", "order"]
template_name = "administration/stage_binding/list.html"
class StageBindingCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
@ -43,7 +49,11 @@ class StageBindingCreateView(
class StageBindingUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
UpdateView,
):
"""Update FlowStageBinding"""

View File

@ -10,25 +10,31 @@ from django.utils.translation import gettext as _
from django.views.generic import ListView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
UserPaginateListMixin,
)
from passbook.lib.views import CreateAssignPermView
from passbook.stages.invitation.forms import InvitationForm
from passbook.stages.invitation.models import Invitation
from passbook.stages.invitation.signals import invitation_created
class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
class InvitationListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
):
"""Show list of all invitations"""
model = Invitation
permission_required = "passbook_stages_invitation.view_invitation"
template_name = "administration/stage_invitation/list.html"
paginate_by = 10
ordering = "-expires"
class InvitationCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,

View File

@ -9,24 +9,30 @@ from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
UserPaginateListMixin,
)
from passbook.lib.views import CreateAssignPermView
from passbook.stages.prompt.forms import PromptAdminForm
from passbook.stages.prompt.models import Prompt
class PromptListView(LoginRequiredMixin, PermissionListMixin, ListView):
class PromptListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
):
"""Show list of all prompts"""
model = Prompt
permission_required = "passbook_stages_prompt.view_prompt"
ordering = "order"
paginate_by = 40
template_name = "administration/stage_prompt/list.html"
class PromptCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
@ -43,7 +49,11 @@ class PromptCreateView(
class PromptUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
UpdateView,
):
"""Update prompt"""

View File

@ -5,17 +5,18 @@ from django.utils.translation import gettext as _
from django.views.generic import ListView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.admin.views.utils import DeleteMessageView, UserPaginateListMixin
from passbook.core.models import Token
class TokenListView(LoginRequiredMixin, PermissionListMixin, ListView):
class TokenListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
):
"""Show list of all tokens"""
model = Token
permission_required = "passbook_core.view_token"
ordering = "expires"
paginate_by = 40
template_name = "administration/token/list.html"

View File

@ -18,18 +18,23 @@ from guardian.mixins import (
)
from passbook.admin.forms.users import UserForm
from passbook.admin.views.utils import DeleteMessageView
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
UserPaginateListMixin,
)
from passbook.core.models import Token, User
from passbook.lib.views import CreateAssignPermView
class UserListView(LoginRequiredMixin, PermissionListMixin, ListView):
class UserListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
):
"""Show list of all users"""
model = User
permission_required = "passbook_core.view_user"
ordering = "username"
paginate_by = 40
template_name = "administration/user/list.html"
def get_queryset(self):
@ -38,6 +43,7 @@ class UserListView(LoginRequiredMixin, PermissionListMixin, ListView):
class UserCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
@ -54,7 +60,11 @@ class UserCreateView(
class UserUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
UpdateView,
):
"""Update user"""

View File

@ -1,9 +1,12 @@
"""passbook admin util views"""
from typing import Any, Dict
from typing import Any, Dict, Optional
from urllib.parse import urlparse
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import QuerySet
from django.http import Http404
from django.http.request import HttpRequest
from django.views.generic import DeleteView, ListView, UpdateView
from passbook.lib.utils.reflection import all_subclasses
@ -40,7 +43,7 @@ class InheritanceCreateView(CreateAssignPermView):
)
except StopIteration as exc:
raise Http404 from exc
return model.form(model)
return model().form
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
@ -61,7 +64,7 @@ class InheritanceUpdateView(UpdateView):
return kwargs
def get_form_class(self):
return self.get_object().form()
return self.get_object().form
def get_object(self, queryset=None):
return (
@ -69,3 +72,31 @@ class InheritanceUpdateView(UpdateView):
.select_subclasses()
.first()
)
class BackSuccessUrlMixin:
"""Checks if a relative URL has been given as ?back param, and redirect to it. Otherwise
default to self.success_url."""
request: HttpRequest
success_url: Optional[str]
def get_success_url(self) -> str:
"""get_success_url from FormMixin"""
back_param = self.request.GET.get("back")
if back_param:
if not bool(urlparse(back_param).netloc):
return back_param
return str(self.success_url)
class UserPaginateListMixin:
"""Get paginate_by value from user's attributes, defaulting to 15"""
request: HttpRequest
# pylint: disable=unused-argument
def get_paginate_by(self, queryset: QuerySet) -> int:
"""get_paginate_by Function of ListView"""
return self.request.user.attributes.get("paginate_by", 15)

View File

@ -0,0 +1,85 @@
"""Audit middleware"""
from functools import partial
from typing import Callable
from django.contrib.auth.models import User
from django.db.models import Model
from django.db.models.signals import post_save, pre_delete
from django.http import HttpRequest, HttpResponse
from passbook.audit.models import Event, EventAction, model_to_dict
from passbook.audit.signals import EventNewThread
from passbook.core.middleware import LOCAL
class AuditMiddleware:
"""Register handlers for duration of request-response that log creation/update/deletion
of models"""
get_response: Callable[[HttpRequest], HttpResponse]
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response
def __call__(self, request: HttpRequest) -> HttpResponse:
# Connect signal for automatic logging
if hasattr(request, "user") and getattr(
request.user, "is_authenticated", False
):
post_save_handler = partial(
self.post_save_handler, user=request.user, request=request
)
pre_delete_handler = partial(
self.pre_delete_handler, user=request.user, request=request
)
post_save.connect(
post_save_handler,
dispatch_uid=LOCAL.passbook["request_id"],
weak=False,
)
pre_delete.connect(
pre_delete_handler,
dispatch_uid=LOCAL.passbook["request_id"],
weak=False,
)
response = self.get_response(request)
post_save.disconnect(dispatch_uid=LOCAL.passbook["request_id"])
pre_delete.disconnect(dispatch_uid=LOCAL.passbook["request_id"])
return response
# pylint: disable=unused-argument
def process_exception(self, request: HttpRequest, exception: Exception):
"""Unregister handlers in case of exception"""
post_save.disconnect(dispatch_uid=LOCAL.passbook["request_id"])
pre_delete.disconnect(dispatch_uid=LOCAL.passbook["request_id"])
@staticmethod
# pylint: disable=unused-argument
def post_save_handler(
user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
):
"""Signal handler for all object's post_save"""
if isinstance(instance, Event):
return
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
EventNewThread(action, request, user=user, model=model_to_dict(instance)).run()
@staticmethod
# pylint: disable=unused-argument
def pre_delete_handler(
user: User, request: HttpRequest, sender, instance: Model, **_
):
"""Signal handler for all object's pre_delete"""
if isinstance(instance, Event):
return
EventNewThread(
EventAction.MODEL_DELETED,
request,
user=user,
model=model_to_dict(instance),
).run()

View File

@ -0,0 +1,59 @@
# Generated by Django 3.1.1 on 2020-09-17 11:55
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
import passbook.audit.models
def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Event = apps.get_model("passbook_audit", "Event")
db_alias = schema_editor.connection.alias
for event in Event.objects.all():
event.delete()
# Because event objects cannot be updated, we have to re-create them
event.pk = None
event.user_json = (
passbook.audit.models.get_user(event.user) if event.user else {}
)
event._state.adding = True
event.save()
class Migration(migrations.Migration):
dependencies = [
("passbook_audit", "0002_auto_20200918_2116"),
]
operations = [
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("LOGIN", "login"),
("LOGIN_FAILED", "login_failed"),
("LOGOUT", "logout"),
("AUTHORIZE_APPLICATION", "authorize_application"),
("SUSPICIOUS_REQUEST", "suspicious_request"),
("SIGN_UP", "sign_up"),
("PASSWORD_RESET", "password_reset"),
("INVITE_CREATED", "invitation_created"),
("INVITE_USED", "invitation_used"),
("IMPERSONATION_STARTED", "impersonation_started"),
("IMPERSONATION_ENDED", "impersonation_ended"),
("CUSTOM", "custom"),
]
),
),
migrations.AddField(
model_name="event", name="user_json", field=models.JSONField(default=dict),
),
migrations.RunPython(convert_user_to_json),
migrations.RemoveField(model_name="event", name="user",),
migrations.RenameField(
model_name="event", old_name="user_json", new_name="user"
),
]

View File

@ -0,0 +1,37 @@
# Generated by Django 3.1.1 on 2020-09-21 18:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_audit", "0003_auto_20200917_1155"),
]
operations = [
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("sign_up", "Sign Up"),
("authorize_application", "Authorize Application"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
]

View File

@ -1,7 +1,6 @@
"""passbook audit models"""
from enum import Enum
from inspect import getmodule, stack
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, Union
from uuid import UUID, uuid4
from django.conf import settings
@ -12,13 +11,17 @@ from django.db.models.base import Model
from django.http import HttpRequest
from django.utils.translation import gettext as _
from django.views.debug import SafeExceptionReporterFilter
from guardian.shortcuts import get_anonymous_user
from guardian.utils import get_anonymous_user
from structlog import get_logger
from passbook.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER
from passbook.core.middleware import (
SESSION_IMPERSONATE_ORIGINAL_USER,
SESSION_IMPERSONATE_USER,
)
from passbook.core.models import User
from passbook.lib.utils.http import get_client_ip
LOGGER = get_logger()
LOGGER = get_logger("passbook.audit")
def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
@ -50,6 +53,22 @@ def model_to_dict(model: Model) -> Dict[str, Any]:
}
def get_user(user: User, original_user: Optional[User] = None) -> Dict[str, Any]:
"""Convert user object to dictionary, optionally including the original user"""
if isinstance(user, AnonymousUser):
user = get_anonymous_user()
user_data = {
"username": user.username,
"pk": user.pk,
"email": user.email,
}
if original_user:
original_data = get_user(original_user)
original_data["on_behalf_of"] = user_data
return original_data
return user_data
def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
"""clean source of all Models that would interfere with the JSONField.
Models are replaced with a dictionary of {
@ -70,38 +89,39 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
return final_dict
class EventAction(Enum):
class EventAction(models.TextChoices):
"""All possible actions to save into the audit log"""
LOGIN = "login"
LOGIN_FAILED = "login_failed"
LOGOUT = "logout"
SIGN_UP = "sign_up"
AUTHORIZE_APPLICATION = "authorize_application"
SUSPICIOUS_REQUEST = "suspicious_request"
SIGN_UP = "sign_up"
PASSWORD_RESET = "password_reset" # noqa # nosec
PASSWORD_SET = "password_set" # noqa # nosec
INVITE_CREATED = "invitation_created"
INVITE_USED = "invitation_used"
SOURCE_LINKED = "source_linked"
IMPERSONATION_STARTED = "impersonation_started"
IMPERSONATION_ENDED = "impersonation_ended"
CUSTOM = "custom"
@staticmethod
def as_choices():
"""Generate choices of actions used for database"""
return tuple(
(x, y.value) for x, y in getattr(EventAction, "__members__").items()
)
MODEL_CREATED = "model_created"
MODEL_UPDATED = "model_updated"
MODEL_DELETED = "model_deleted"
CUSTOM_PREFIX = "custom_"
class Event(models.Model):
"""An individual audit log event"""
event_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
)
action = models.TextField(choices=EventAction.as_choices())
user = models.JSONField(default=dict)
action = models.TextField(choices=EventAction.choices)
date = models.DateTimeField(auto_now_add=True)
app = models.TextField()
context = models.JSONField(default=dict, blank=True)
@ -116,20 +136,18 @@ class Event(models.Model):
@staticmethod
def new(
action: EventAction,
action: Union[str, EventAction],
app: Optional[str] = None,
_inspect_offset: int = 1,
**kwargs,
) -> "Event":
"""Create new Event instance from arguments. Instance is NOT saved."""
if not isinstance(action, EventAction):
raise ValueError(
f"action must be EventAction instance but was {type(action)}"
)
action = EventAction.CUSTOM_PREFIX + action
if not app:
app = getmodule(stack()[_inspect_offset][0]).__name__
cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs))
event = Event(action=action.value, app=app, context=cleaned_kwargs)
event = Event(action=action, app=app, context=cleaned_kwargs)
return event
def from_http(
@ -139,17 +157,18 @@ class Event(models.Model):
Events independently from requests.
`user` arguments optionally overrides user from requests."""
if hasattr(request, "user"):
if isinstance(request.user, AnonymousUser):
self.user = get_anonymous_user()
else:
self.user = request.user
self.user = get_user(
request.user,
request.session.get(SESSION_IMPERSONATE_ORIGINAL_USER, None),
)
if user:
self.user = user
self.user = get_user(user)
# Check if we're currently impersonating, and add that user
if hasattr(request, "session"):
if SESSION_IMPERSONATE_ORIGINAL_USER in request.session:
self.context["on_behalf_of"] = model_to_dict(
request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
self.user = get_user(request.session[SESSION_IMPERSONATE_ORIGINAL_USER])
self.user["on_behalf_of"] = get_user(
request.session[SESSION_IMPERSONATE_USER]
)
# User 255.255.255.255 as fallback if IP cannot be determined
self.client_ip = get_client_ip(request) or "255.255.255.255"

View File

@ -20,15 +20,18 @@ from passbook.stages.user_write.signals import user_write
class EventNewThread(Thread):
"""Create Event in background thread"""
action: EventAction
action: str
request: HttpRequest
kwargs: Dict[str, Any]
user: Optional[User] = None
def __init__(self, action: EventAction, request: HttpRequest, **kwargs):
def __init__(
self, action: str, request: HttpRequest, user: Optional[User] = None, **kwargs
):
super().__init__()
self.action = action
self.request = request
self.user = user
self.kwargs = kwargs
def run(self):
@ -57,7 +60,7 @@ def on_user_logged_out(sender, request: HttpRequest, user: User, **_):
# pylint: disable=unused-argument
def on_user_write(sender, request: HttpRequest, user: User, data: Dict[str, Any], **_):
"""Log User write"""
thread = EventNewThread(EventAction.CUSTOM, request, **data)
thread = EventNewThread("stages/user_write", request, **data)
thread.user = user
thread.run()

View File

@ -40,12 +40,28 @@
</div>
</th>
<td role="cell">
<div>
<div>
<code>{{ entry.context }}</code>
</div>
{% if entry.user.on_behalf_of %}
<small>
{% blocktrans with username=entry.user.on_behalf_of.username %}
On behalf of {{ username }}
{% endblocktrans %}
</small>
{% endif %}
</div>
</td>
<td role="cell">
<span>
{{ entry.user }}
</span>
<div>
<div>{{ entry.user.username }}</div>
<small>
{% blocktrans with pk=entry.user.pk %}
ID: {{ pk }}
{% endblocktrans %}
</small>
</div>
</td>
<td role="cell">
<span>

View File

@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from passbook.audit.models import Event, EventAction
from passbook.audit.models import Event
from passbook.policies.dummy.models import DummyPolicy
@ -13,7 +13,7 @@ class TestAuditEvent(TestCase):
def test_new_with_model(self):
"""Create a new Event passing a model as kwarg"""
event = Event.new(EventAction.CUSTOM, test={"model": get_anonymous_user()})
event = Event.new("unittest", test={"model": get_anonymous_user()})
event.save() # We save to ensure nothing is un-saveable
model_content_type = ContentType.objects.get_for_model(get_anonymous_user())
self.assertEqual(
@ -24,7 +24,7 @@ class TestAuditEvent(TestCase):
def test_new_with_uuid_model(self):
"""Create a new Event passing a model (with UUID PK) as kwarg"""
temp_model = DummyPolicy.objects.create(name="test", result=True)
event = Event.new(EventAction.CUSTOM, model=temp_model)
event = Event.new("unittest", model=temp_model)
event.save() # We save to ensure nothing is un-saveable
model_content_type = ContentType.objects.get_for_model(temp_model)
self.assertEqual(

View File

@ -3,14 +3,16 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView
from guardian.mixins import PermissionListMixin
from passbook.admin.views.utils import UserPaginateListMixin
from passbook.audit.models import Event
class EventListView(PermissionListMixin, LoginRequiredMixin, ListView):
class EventListView(
PermissionListMixin, LoginRequiredMixin, UserPaginateListMixin, ListView
):
"""Show list of all invitations"""
model = Event
template_name = "audit/list.html"
permission_required = "passbook_audit.view_event"
ordering = "-created"
paginate_by = 20

View File

@ -20,5 +20,5 @@ def admin_autoregister(app: AppConfig):
for _app in apps.get_app_configs():
if _app.label.startswith("passbook_"):
LOGGER.debug("Registering application for dj-admin", app=_app.label)
LOGGER.debug("Registering application for dj-admin", application=_app.label)
admin_autoregister(_app)

37
passbook/core/channels.py Normal file
View File

@ -0,0 +1,37 @@
"""Channels base classes"""
from channels.generic.websocket import JsonWebsocketConsumer
from django.core.exceptions import ValidationError
from structlog import get_logger
from passbook.core.models import Token, TokenIntents, User
LOGGER = get_logger()
class AuthJsonConsumer(JsonWebsocketConsumer):
"""Authorize a client with a token"""
user: User
def connect(self):
headers = dict(self.scope["headers"])
if b"authorization" not in headers:
LOGGER.warning("WS Request without authorization header")
self.close()
token = headers[b"authorization"]
try:
token_uuid = token.decode("utf-8")
tokens = Token.filter_not_expired(
token_uuid=token_uuid, intent=TokenIntents.INTENT_API
)
if not tokens.exists():
LOGGER.warning("WS Request with invalid token")
self.close()
return False
except ValidationError:
LOGGER.warning("WS Invalid UUID")
self.close()
return False
self.user = tokens.first().user
return True

View File

@ -1,11 +1,14 @@
"""passbook admin Middleware to impersonate users"""
from logging import Logger
from threading import local
from typing import Callable
from uuid import uuid4
from django.http import HttpRequest, HttpResponse
SESSION_IMPERSONATE_USER = "passbook_impersonate_user"
SESSION_IMPERSONATE_ORIGINAL_USER = "passbook_impersonate_original_user"
LOCAL = local()
class ImpersonateMiddleware:
@ -24,3 +27,30 @@ class ImpersonateMiddleware:
request.user = request.session[SESSION_IMPERSONATE_USER]
return self.get_response(request)
class RequestIDMiddleware:
"""Add a unique ID to every request"""
get_response: Callable[[HttpRequest], HttpResponse]
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response
def __call__(self, request: HttpRequest) -> HttpResponse:
if not hasattr(request, "request_id"):
request_id = uuid4().hex
setattr(request, "request_id", request_id)
LOCAL.passbook = {"request_id": request_id}
response = self.get_response(request)
response["X-passbook-id"] = request.request_id
del LOCAL.passbook["request_id"]
return response
# pylint: disable=unused-argument
def structlog_add_request_id(logger: Logger, method_name: str, event_dict):
"""If threadlocal has passbook defined, add request_id to log"""
if hasattr(LOCAL, "passbook"):
event_dict["request_id"] = LOCAL.passbook.get("request_id", "")
return event_dict

View File

@ -14,7 +14,7 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
pbadmin, _ = User.objects.using(db_alias).get_or_create(
username="pbadmin", email="root@localhost", name="passbook Default Admin"
)
pbadmin.set_password("pbadmin") # noqa # nosec
pbadmin.set_password("pbadmin", signal=False) # noqa # nosec
pbadmin.save()

View File

@ -1,6 +1,6 @@
"""passbook core models"""
from datetime import timedelta
from typing import Any, Optional, Type
from typing import Any, Dict, Optional, Type
from uuid import uuid4
from django.contrib.auth.models import AbstractUser
@ -9,6 +9,7 @@ from django.db import models
from django.db.models import Q, QuerySet
from django.forms import ModelForm
from django.http import HttpRequest
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from guardian.mixins import GuardianUserMixin
@ -80,7 +81,16 @@ class User(GuardianUserMixin, AbstractUser):
objects = UserManager()
@property
def group_attributes(self) -> Dict[str, Any]:
"""Get a dictionary containing the attributes from all groups the user belongs to,
including the users attributes"""
final_attributes = {}
for group in self.pb_groups.all().order_by("name"):
final_attributes.update(group.attributes)
final_attributes.update(self.attributes)
return final_attributes
@cached_property
def is_superuser(self) -> bool:
"""Get supseruser status based on membership in a group with superuser status"""
return self.pb_groups.filter(is_superuser=True).exists()
@ -88,10 +98,10 @@ class User(GuardianUserMixin, AbstractUser):
@property
def is_staff(self) -> bool:
"""superuser == staff user"""
return self.is_superuser
return self.is_superuser # type: ignore
def set_password(self, password):
if self.pk:
def set_password(self, password, signal=True):
if self.pk and signal:
password_changed.send(sender=self, user=self, password=password)
self.password_change_date = now()
return super().set_password(password)
@ -128,6 +138,7 @@ class Provider(models.Model):
Can return None for providers that are not URL-based"""
return None
@property
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError
@ -212,6 +223,7 @@ class Source(PolicyBindingModel):
objects = InheritanceManager()
@property
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError
@ -313,6 +325,7 @@ class PropertyMapping(models.Model):
objects = InheritanceManager()
@property
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError

View File

@ -44,12 +44,12 @@
</div>
<div class="pf-c-page__header-tools">
<div class="pf-c-page__header-tools-group pf-m-icons">
<a href="{% url 'passbook_flows:default-invalidation' %}" class="pf-c-button pf-m-plain" type="button" aria-label="logout">
<a href="{% url 'passbook_flows:default-invalidation' %}" class="pf-c-button pf-m-plain" type="button" id="logout">
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
</a>
</div>
<div class="pf-c-page__header-tools-group">
<a href="{% url 'passbook_core:user-settings' %}" class="pf-c-button">
<a href="{% url 'passbook_core:user-settings' %}" id="user-settings" class="pf-c-button">
{{ user.username }}
</a>
</div>

View File

@ -53,7 +53,7 @@
<div class="pf-c-empty-state__body">
{% trans "Either no applications are defined, or you don't have access to any." %}
</div>
{% if user.is_superuser %} {# TODO:use guardian permissions instead #}
{% if perms.passbook_core.add_application %}
<a href="{% url 'passbook_admin:application-create' %}" class="pf-c-button pf-m-primary" type="button">
{% trans 'Create Application' %}
</a>

View File

@ -62,6 +62,6 @@ class ServerErrorView(TemplateView):
template_name = "error/generic.html"
# pylint: disable=useless-super-delegation
def dispatch(self, *args, **kwargs):
def dispatch(self, *args, **kwargs): # pragma: no cover
"""Little wrapper so django accepts this function"""
return super().dispatch(*args, **kwargs)

View File

@ -31,7 +31,7 @@ class ImpersonateInitView(View):
request.session[SESSION_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_IMPERSONATE_USER] = user_to_be
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request)
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
return redirect("passbook_core:overview")
@ -48,9 +48,11 @@ class ImpersonateEndView(View):
LOGGER.debug("Can't end impersonation", user=request.user)
return redirect("passbook_core:overview")
original_user = request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
del request.session[SESSION_IMPERSONATE_USER]
del request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request)
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
return redirect("passbook_core:overview")

50
passbook/crypto/tests.py Normal file
View File

@ -0,0 +1,50 @@
"""Crypto tests"""
from django.test import TestCase
from passbook.crypto.api import CertificateKeyPairSerializer
from passbook.crypto.forms import CertificateKeyPairForm
from passbook.crypto.models import CertificateKeyPair
class TestCrypto(TestCase):
"""Test Crypto validation"""
def test_form(self):
"""Test form validation"""
keypair = CertificateKeyPair.objects.first()
self.assertTrue(
CertificateKeyPairForm(
{
"name": keypair.name,
"certificate_data": keypair.certificate_data,
"key_data": keypair.key_data,
}
).is_valid()
)
self.assertFalse(
CertificateKeyPairForm(
{"name": keypair.name, "certificate_data": "test", "key_data": "test"}
).is_valid()
)
def test_serializer(self):
"""Test API Validation"""
keypair = CertificateKeyPair.objects.first()
self.assertTrue(
CertificateKeyPairSerializer(
data={
"name": keypair.name,
"certificate_data": keypair.certificate_data,
"key_data": keypair.key_data,
}
).is_valid()
)
self.assertFalse(
CertificateKeyPairSerializer(
data={
"name": keypair.name,
"certificate_data": "test",
"key_data": "test",
}
).is_valid()
)

View File

@ -0,0 +1,116 @@
"""passbook benchmark command"""
from csv import DictWriter
from multiprocessing import Manager, Process, cpu_count
from sys import stdout
from time import time
from typing import List
from django import db
from django.core.management.base import BaseCommand
from django.test import RequestFactory
from structlog import get_logger
from passbook import __version__
from passbook.core.models import User
from passbook.flows.models import Flow
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
LOGGER = get_logger()
class FlowPlanProcess(Process): # pragma: no cover
"""Test process which executes flow planner"""
def __init__(self, index, return_dict, flow, user) -> None:
super().__init__()
self.index = index
self.return_dict = return_dict
self.flow = flow
self.user = user
self.request = RequestFactory().get("/")
def run(self):
print(f"Proc {self.index} Running")
def test_inner():
planner = FlowPlanner(self.flow)
planner.use_cache = False
planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: self.user})
diffs = []
for _ in range(1000):
start = time()
test_inner()
end = time()
diffs.append(end - start)
self.return_dict[self.index] = diffs
class Command(BaseCommand): # pragma: no cover
"""Benchmark passbook"""
def add_arguments(self, parser):
parser.add_argument(
"-p",
"--processes",
default=cpu_count(),
action="store",
help="How many processes should be started.",
)
parser.add_argument(
"--csv", action="store_true", help="Output results as CSV",
)
def benchmark_flows(self, proc_count) -> str:
"""Get full recovery link"""
flow = Flow.objects.get(slug="default-authentication-flow")
user = User.objects.get(username="pbadmin")
manager = Manager()
return_dict = manager.dict()
jobs = []
db.connections.close_all()
for i in range(proc_count):
proc = FlowPlanProcess(i, return_dict, flow, user)
jobs.append(proc)
proc.start()
for proc in jobs:
proc.join()
return return_dict.values()
def handle(self, *args, **options):
"""Start benchmark"""
proc_count = options.get("processes", 1)
all_values = self.benchmark_flows(proc_count)
if options.get("csv"):
self.output_csv(all_values)
else:
self.output_overview(all_values)
def output_overview(self, values: List[List[int]]):
"""Output results human readable"""
total_max = max([max(inner) for inner in values])
total_min = min([min(inner) for inner in values])
total_avg = sum([sum(inner) for inner in values]) / sum(
[len(inner) for inner in values]
)
print(f"Version: {__version__}")
print(f"Processes: {len(values)}")
print(f"\tMax: {total_max * 100}ms")
print(f"\tMin: {total_min * 100}ms")
print(f"\tAvg: {total_avg * 100}ms")
def output_csv(self, values: List[List[int]]):
"""Output results as CSV"""
proc_count = len(values)
fieldnames = [f"proc_{idx}" for idx in range(proc_count)]
writer = DictWriter(stdout, fieldnames=fieldnames)
writer.writeheader()
for run_idx in range(len(values[0])):
row_dict = {}
for proc_idx in range(proc_count):
row_dict[f"proc_{proc_idx}"] = values[proc_idx][run_idx] * 100
writer.writerow(row_dict)

View File

@ -12,7 +12,7 @@ FLOW_POLICY_EXPRESSION = """# This policy ensures that this flow can only be use
return pb_is_sso_flow"""
PROMPT_POLICY_EXPRESSION = """# Check if we've not been given a username by the external IdP
# and trigger the enrollment flow
return 'username' not in pb_flow_plan.context.get('prompt_data', {})"""
return 'username' not in context.get('prompt_data', {})"""
def create_default_source_enrollment_flow(
@ -80,7 +80,9 @@ def create_default_source_enrollment_flow(
)
binding, _ = FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow, stage=prompt_stage, defaults={"order": 0}
target=flow,
stage=prompt_stage,
defaults={"order": 0, "re_evaluate_policies": True},
)
PolicyBinding.objects.using(db_alias).update_or_create(
policy=prompt_policy, target=binding, defaults={"order": 0}

View File

@ -0,0 +1,44 @@
# Generated by Django 3.1.1 on 2020-09-24 16:05
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from passbook.flows.models import FlowDesignation
def update_flow_designation(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Flow = apps.get_model("passbook_flows", "Flow")
db_alias = schema_editor.connection.alias
for flow in Flow.objects.using(db_alias).all():
if flow.designation == "stage_setup":
flow.designation = FlowDesignation.STAGE_CONFIGURATION
flow.save()
class Migration(migrations.Migration):
dependencies = [
("passbook_flows", "0012_auto_20200908_1542"),
]
operations = [
migrations.AlterField(
model_name="flow",
name="designation",
field=models.CharField(
choices=[
("authentication", "Authentication"),
("authorization", "Authorization"),
("invalidation", "Invalidation"),
("enrollment", "Enrollment"),
("unenrollment", "Unrenollment"),
("recovery", "Recovery"),
("stage_configuration", "Stage Configuration"),
],
max_length=100,
),
),
migrations.RunPython(update_flow_designation),
]

View File

@ -0,0 +1,51 @@
# Generated by Django 3.1.1 on 2020-09-25 23:32
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
# First stage for default-source-enrollment flow (prompt stage)
# needs to have its policy re-evaluated
def update_default_source_enrollment_flow_binding(
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
):
Flow = apps.get_model("passbook_flows", "Flow")
FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding")
db_alias = schema_editor.connection.alias
flows = Flow.objects.using(db_alias).filter(slug="default-source-enrollment")
if not flows.exists():
return
flow = flows.first()
binding = FlowStageBinding.objects.get(target=flow, order=0)
binding.re_evaluate_policies = True
binding.save()
class Migration(migrations.Migration):
dependencies = [
("passbook_flows", "0013_auto_20200924_1605"),
]
operations = [
migrations.AlterModelOptions(
name="flowstagebinding",
options={
"ordering": ["target", "order"],
"verbose_name": "Flow Stage Binding",
"verbose_name_plural": "Flow Stage Bindings",
},
),
migrations.AlterField(
model_name="flowstagebinding",
name="re_evaluate_policies",
field=models.BooleanField(
default=False,
help_text="When this option is enabled, the planner will re-evaluate policies bound to this binding.",
),
),
migrations.RunPython(update_default_source_enrollment_flow_binding),
]

View File

@ -37,7 +37,7 @@ class FlowDesignation(models.TextChoices):
ENROLLMENT = "enrollment"
UNRENOLLMENT = "unenrollment"
RECOVERY = "recovery"
STAGE_SETUP = "stage_setup"
STAGE_CONFIGURATION = "stage_configuration"
class Stage(SerializerModel):
@ -50,6 +50,7 @@ class Stage(SerializerModel):
objects = InheritanceManager()
@property
def type(self) -> Type["StageView"]:
"""Return StageView class that implements logic for this stage"""
# This is a bit of a workaround, since we can't set class methods with setattr
@ -57,6 +58,7 @@ class Stage(SerializerModel):
return getattr(self, "__in_memory_type")
raise NotImplementedError
@property
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError
@ -155,7 +157,10 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
re_evaluate_policies = models.BooleanField(
default=False,
help_text=_(
"When this option is enabled, the planner will re-evaluate policies bound to this."
(
"When this option is enabled, the planner will re-evaluate "
"policies bound to this binding."
)
),
)
@ -170,12 +175,35 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
return FlowStageBindingSerializer
def __str__(self) -> str:
return f"'{self.target}' -> '{self.stage}' # {self.order}"
return f"{self.target} #{self.order} -> {self.stage}"
class Meta:
ordering = ["order", "target"]
ordering = ["target", "order"]
verbose_name = _("Flow Stage Binding")
verbose_name_plural = _("Flow Stage Bindings")
unique_together = (("target", "stage", "order"),)
class ConfigurableStage(models.Model):
"""Abstract base class for a Stage that can be configured by the enduser.
The stage should create a default flow with the configure_stage designation during
migration."""
configure_flow = models.ForeignKey(
Flow,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text=_(
(
"Flow used by an authenticated user to configure this Stage. "
"If empty, user will not be able to configure this stage."
)
),
)
class Meta:
abstract = True

View File

@ -0,0 +1,31 @@
"""flow model tests"""
from typing import Callable, Type
from django.forms import ModelForm
from django.test import TestCase
from passbook.flows.models import Stage
from passbook.flows.stage import StageView
class TestStageProperties(TestCase):
"""Generic model properties tests"""
def stage_tester_factory(model: Type[Stage]) -> Callable:
"""Test a form"""
def tester(self: TestStageProperties):
model_inst = model()
self.assertTrue(issubclass(model_inst.form, ModelForm))
self.assertTrue(issubclass(model_inst.type, StageView))
return tester
for stage_type in Stage.__subclasses__():
setattr(
TestStageProperties,
f"test_stage_{stage_type.__name__}",
stage_tester_factory(stage_type),
)

View File

@ -3,6 +3,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
from django.contrib.sessions.middleware import SessionMiddleware
from django.core.cache import cache
from django.http import HttpRequest
from django.shortcuts import reverse
from django.test import RequestFactory, TestCase
from guardian.shortcuts import get_anonymous_user
@ -23,6 +24,11 @@ CACHE_MOCK = Mock(wraps=cache)
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
def dummy_get_response(request: HttpRequest): # pragma: no cover
"""Dummy get_response for SessionMiddleware"""
return None
class TestFlowPlanner(TestCase):
"""Test planner logic"""
@ -164,7 +170,7 @@ class TestFlowPlanner(TestCase):
)
request.user = get_anonymous_user()
middleware = SessionMiddleware()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()

View File

@ -105,15 +105,10 @@ class TestFlowTransfer(TransactionTestCase):
order=2,
type=FieldTypes.PASSWORD,
)
# Password checking policy
password_policy = ExpressionPolicy.objects.create(
name=generate_client_id(), expression="return True",
)
# Stages
first_stage = PromptStage.objects.create(name=generate_client_id())
first_stage.fields.set([username_prompt, password, password_repeat])
first_stage.validation_policies.set([password_policy])
first_stage.save()
flow = Flow.objects.create(

View File

@ -4,6 +4,7 @@ from django.urls import path
from passbook.flows.models import FlowDesignation
from passbook.flows.views import (
CancelView,
ConfigureFlowInitView,
FlowExecutorShellView,
FlowExecutorView,
ToDefaultFlow,
@ -36,6 +37,11 @@ urlpatterns = [
name="default-unenrollment",
),
path("-/cancel/", CancelView.as_view(), name="cancel"),
path(
"-/configure/<uuid:stage_uuid>/",
ConfigureFlowInitView.as_view(),
name="configure",
),
path("b/<slug:flow_slug>/", FlowExecutorView.as_view(), name="flow-executor"),
path(
"<slug:flow_slug>/", FlowExecutorShellView.as_view(), name="flow-executor-shell"

View File

@ -2,6 +2,7 @@
from traceback import format_tb
from typing import Any, Dict, Optional
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import (
Http404,
HttpRequest,
@ -19,8 +20,8 @@ from structlog import get_logger
from passbook.audit.models import cleanse_dict
from passbook.core.models import PASSBOOK_USER_DEBUG
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from passbook.flows.models import Flow, FlowDesignation, Stage
from passbook.flows.planner import FlowPlan, FlowPlanner
from passbook.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan, FlowPlanner
from passbook.lib.utils.reflection import class_to_path
from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs
from passbook.policies.http import AccessDeniedResponse
@ -95,7 +96,7 @@ class FlowExecutorView(View):
current_stage=self.current_stage,
flow_slug=self.flow.slug,
)
stage_cls = self.current_stage.type()
stage_cls = self.current_stage.type
self.current_stage_view = stage_cls(self)
self.current_stage_view.args = self.args
self.current_stage_view.kwargs = self.kwargs
@ -156,10 +157,6 @@ class FlowExecutorView(View):
stage_class=class_to_path(self.current_stage_view.__class__),
flow_slug=self.flow.slug,
)
# We call plan.next here to check for re-evaluate markers
# this is important so we can save the result
# and we don't have to re-evaluate the policies each request
self.plan.next()
self.plan.pop()
self.request.session[SESSION_KEY_PLAN] = self.plan
if self.plan.stages:
@ -295,3 +292,32 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
{"type": "template", "body": source.content.decode("utf-8")}
)
return source
class ConfigureFlowInitView(LoginRequiredMixin, View):
"""Initiate planner for selected change flow and redirect to flow executor,
or raise Http404 if no configure_flow has been set."""
def get(self, request: HttpRequest, stage_uuid: str) -> HttpResponse:
"""Initiate planner for selected change flow and redirect to flow executor,
or raise Http404 if no configure_flow has been set."""
try:
stage: Stage = Stage.objects.get_subclass(pk=stage_uuid)
except Stage.DoesNotExist as exc:
raise Http404 from exc
if not isinstance(stage, ConfigurableStage):
LOGGER.debug("Stage does not inherit ConfigurableStage", stage=stage)
raise Http404
if not stage.configure_flow:
LOGGER.debug("Stage has no configure_flow set", stage=stage)
raise Http404
plan = FlowPlanner(stage.configure_flow).plan(
request, {PLAN_CONTEXT_PENDING_USER: request.user}
)
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"passbook_flows:flow-executor-shell",
self.request.GET,
flow_slug=stage.configure_flow.slug,
)

View File

@ -1,9 +1,23 @@
"""logging helpers"""
from logging import Logger
from os import getpid
from typing import Callable
# pylint: disable=unused-argument
def add_process_id(logger, method_name, event_dict):
def add_process_id(logger: Logger, method_name: str, event_dict):
"""Add the current process ID"""
event_dict["pid"] = getpid()
return event_dict
def add_common_fields(environment: str) -> Callable:
"""Add a common field to easily search for passbook logs"""
def add_common_field(logger: Logger, method_name: str, event_dict):
"""Add a common field to easily search for passbook logs"""
event_dict["app"] = "passbook"
event_dict["app_environment"] = environment
return event_dict
return add_common_field

View File

@ -10,7 +10,6 @@ from redis.exceptions import ConnectionError as RedisConnectionError
from redis.exceptions import RedisError
from rest_framework.exceptions import APIException
from structlog import get_logger
from websockets.exceptions import WebSocketException
LOGGER = get_logger()
@ -38,7 +37,6 @@ def before_send(event, hint):
OSError,
RedisError,
SentryIgnoredException,
WebSocketException,
CeleryError,
LDAPException,
)

View File

@ -8,7 +8,7 @@ LOGGER = get_logger()
@CELERY_APP.task()
def backup_database():
def backup_database(): # pragma: no cover
"""Backup database"""
management.call_command("dbbackup")
LOGGER.info("Successfully backed up database.")

View File

@ -17,7 +17,7 @@ LOGGER = get_logger()
@register.simple_tag(takes_context=True)
def back(context: Context) -> str:
"""Return a link back (either from GET paramter or referer."""
"""Return a link back (either from GET parameter or referer."""
if "request" not in context:
return ""
request = context.get("request")

30
passbook/lib/tests.py Normal file
View File

@ -0,0 +1,30 @@
"""base model tests"""
from typing import Callable, Type
from django.test import TestCase
from rest_framework.serializers import BaseSerializer
from passbook.flows.models import Stage
from passbook.lib.models import SerializerModel
from passbook.lib.utils.reflection import all_subclasses
class TestModels(TestCase):
"""Generic model properties tests"""
def model_tester_factory(test_model: Type[Stage]) -> Callable:
"""Test a form"""
def tester(self: TestModels):
model_inst = test_model()
try:
self.assertTrue(issubclass(model_inst.serializer, BaseSerializer))
except NotImplementedError:
pass
return tester
for model in all_subclasses(SerializerModel):
setattr(TestModels, f"test_model_{model.__name__}", model_tester_factory(model))

View File

@ -14,7 +14,7 @@ def _get_client_ip_from_meta(meta: Dict[str, Any]) -> Optional[str]:
)
for _header in headers:
if _header in meta:
return meta.get(_header)
return meta.get(_header).split(", ")[0]
return None

View File

@ -4,14 +4,13 @@ from enum import IntEnum
from time import time
from typing import Any, Dict
from channels.generic.websocket import JsonWebsocketConsumer
from dacite import from_dict
from dacite.data import Data
from django.core.cache import cache
from django.core.exceptions import ValidationError
from guardian.shortcuts import get_objects_for_user
from structlog import get_logger
from passbook.core.models import Token, TokenIntents
from passbook.core.channels import AuthJsonConsumer
from passbook.outposts.models import Outpost
LOGGER = get_logger()
@ -38,33 +37,18 @@ class WebsocketMessage:
args: Dict[str, Any] = field(default_factory=dict)
class OutpostConsumer(JsonWebsocketConsumer):
class OutpostConsumer(AuthJsonConsumer):
"""Handler for Outposts that connect over websockets for health checks and live updates"""
outpost: Outpost
def connect(self):
# TODO: This authentication block could be handeled in middleware
headers = dict(self.scope["headers"])
if b"authorization" not in headers:
LOGGER.warning("WS Request without authorization header")
self.close()
token = headers[b"authorization"]
try:
token_uuid = token.decode("utf-8")
tokens = Token.filter_not_expired(
token_uuid=token_uuid, intent=TokenIntents.INTENT_API
)
if not tokens.exists():
LOGGER.warning("WS Request with invalid token")
self.close()
except ValidationError:
LOGGER.warning("WS Invalid UUID")
self.close()
if not super().connect():
return
uuid = self.scope["url_route"]["kwargs"]["pk"]
outpost = Outpost.objects.filter(pk=uuid)
outpost = get_objects_for_user(
self.user, "passbook_outposts.view_outpost"
).filter(pk=uuid)
if not outpost.exists():
self.close()
return

View File

@ -31,6 +31,7 @@ class DummyPolicy(Policy):
return DummyPolicySerializer
@property
def form(self) -> Type[ModelForm]:
from passbook.policies.dummy.forms import DummyPolicyForm

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