Compare commits

..

313 Commits

Author SHA1 Message Date
316ac78e49 new release: 0.9.0-stable 2020-08-01 22:17:59 +02:00
de2b67b111 providers/app_gw: improve templates 2020-08-01 22:13:12 +02:00
e1bbbe6671 providers/app_gw: disable client authz to use passbook 2020-08-01 20:53:55 +02:00
8b3839343c providers/oidc: remove static lookup for OIDCProvider, get related object for app_gw 2020-08-01 20:53:33 +02:00
7897ca4744 providers/app_gw: fix Client scopes and URLs 2020-08-01 20:33:38 +02:00
2fd00c6c9d Merge pull request #149 from BeryJu/dependabot/pip/boto3-1.14.33
build(deps): bump boto3 from 1.14.31 to 1.14.33
2020-08-01 20:25:20 +02:00
80f7f82fa4 build(deps): bump boto3 from 1.14.31 to 1.14.33
Bumps [boto3](https://github.com/boto/boto3) from 1.14.31 to 1.14.33.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.31...1.14.33)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-08-01 18:10:27 +00:00
1a21012911 providers/app_gw: fix URL Validation not working for internal and external host 2020-08-01 20:02:43 +02:00
d4a5269bf1 *: Adjust forms to only show respective types of Flows and PropertyMappings 2020-08-01 20:02:23 +02:00
fcf70a3cd4 providers/app_gw: Fix K8s template labels, add missing ISSUER_URL 2020-08-01 19:47:40 +02:00
e9411d856c docs: add notice that AWS requires POST binding 2020-08-01 19:42:33 +02:00
1a6dd00681 providers/saml: fix X509Data container linebreaks 2020-08-01 19:38:59 +02:00
330bd0932b providers/saml: fix NotOnOrAfter using incorrect timestamp 2020-08-01 19:38:41 +02:00
250e77f40f Merge pull request #147 from BeryJu/dependabot/pip/boto3-1.14.31
build(deps): bump boto3 from 1.14.30 to 1.14.31
2020-07-30 09:18:52 +02:00
ef71aba544 build(deps): bump boto3 from 1.14.30 to 1.14.31
Bumps [boto3](https://github.com/boto/boto3) from 1.14.30 to 1.14.31.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.30...1.14.31)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-30 05:18:20 +00:00
567a8f53da ci: add codeql analysis 2020-07-29 09:14:17 +02:00
88c87aa205 Merge pull request #146 from BeryJu/dependabot/pip/boto3-1.14.30
build(deps): bump boto3 from 1.14.28 to 1.14.30
2020-07-29 09:12:33 +02:00
90ac3d56ca build(deps): bump boto3 from 1.14.28 to 1.14.30
Bumps [boto3](https://github.com/boto/boto3) from 1.14.28 to 1.14.30.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.28...1.14.30)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-29 05:20:31 +00:00
a298e9e2ca Merge pull request #144 from BeryJu/dependabot/pip/coverage-5.2.1
build(deps-dev): bump coverage from 5.2 to 5.2.1
2020-07-27 08:59:32 +02:00
abdf86d9c9 build(deps-dev): bump coverage from 5.2 to 5.2.1
Bumps [coverage](https://github.com/nedbat/coveragepy) from 5.2 to 5.2.1.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/coverage-5.2...coverage-5.2.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-27 05:23:03 +00:00
c58658d820 new release: 0.9.0-rc2 2020-07-26 23:34:36 +02:00
a9b5e6ea13 flows: fix shell not showing spinner after submit 2020-07-26 22:15:50 +02:00
ddb0fdee98 providers/app_gw: generate docker-compose in code 2020-07-26 22:01:37 +02:00
83205f1b49 providers/app_gw: use full URL with protocol for internal/external_host 2020-07-26 22:01:20 +02:00
7221800a16 Merge pull request #143 from BeryJu/dependabot/pip/boto3-1.14.28
build(deps): bump boto3 from 1.14.26 to 1.14.28
2020-07-26 00:10:20 +02:00
4515cb6bbe build(deps): bump boto3 from 1.14.26 to 1.14.28
Bumps [boto3](https://github.com/boto/boto3) from 1.14.26 to 1.14.28.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.26...1.14.28)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-25 21:00:05 +00:00
7f9da11eba Merge pull request #139 from BeryJu/dependabot/pip/pylint-django-2.2.0
build(deps-dev): bump pylint-django from 2.1.0 to 2.2.0
2020-07-25 22:53:05 +02:00
da69d2611d build(deps-dev): bump pylint-django from 2.1.0 to 2.2.0
Bumps [pylint-django](https://github.com/PyCQA/pylint-django) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/PyCQA/pylint-django/releases)
- [Changelog](https://github.com/PyCQA/pylint-django/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/PyCQA/pylint-django/compare/v2.1.0...v2.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-25 20:20:48 +00:00
3b4be5695a gatekeeper: fix non-existent templates being copied 2020-07-25 22:13:15 +02:00
9d68c9550b gatekeeper: automatically redirect to passbook 2020-07-25 21:45:36 +02:00
3b2d469780 e2e: ensure that PasswordStage's change_flow is set correctly 2020-07-25 21:37:22 +02:00
ae629d1159 providers/oauth: remove LoginRequired from AuthorizationFlowInitView as user is redirected within 2020-07-25 21:36:50 +02:00
72a6f9cbe0 providers/saml: remove LoginRequired from SAMLSSOView as user is redirected within 2020-07-25 21:36:28 +02:00
9793b7461b providers/oidc: remove LoginRequired from AuthorizationFlowInitView as user is redirected within 2020-07-25 21:35:38 +02:00
9c1a824dc4 providers/app_gw: fix Issuer URL being incorrect, fix incorrect length cookie secret 2020-07-25 21:34:14 +02:00
738ced3327 e2e: CI -> TF_BUILD 2020-07-23 20:03:35 +02:00
ed1ee1fa55 Merge pull request #138 from BeryJu/dependabot/pip/boto3-1.14.26
build(deps): bump boto3 from 1.14.25 to 1.14.26
2020-07-23 19:49:54 +02:00
95776bbc56 build(deps): bump boto3 from 1.14.25 to 1.14.26
Bumps [boto3](https://github.com/boto/boto3) from 1.14.25 to 1.14.26.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.25...1.14.26)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-23 17:48:38 +00:00
62a4beb3d6 Merge pull request #140 from BeryJu/dependabot/pip/sentry-sdk-0.16.2
build(deps): bump sentry-sdk from 0.16.1 to 0.16.2
2020-07-23 19:45:08 +02:00
466a825f5b Merge pull request #141 from BeryJu/dependabot/pip/urllib3-1.25.10
build(deps): bump urllib3 from 1.25.9 to 1.25.10
2020-07-23 19:44:52 +02:00
3ffed279d7 e2e: fix flow setup stage test not finding link 2020-07-23 11:04:59 +02:00
4b6b36b2d2 build(deps): bump urllib3 from 1.25.9 to 1.25.10
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.9 to 1.25.10.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/1.25.10/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.25.9...1.25.10)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-23 05:18:49 +00:00
2a8f63bf86 build(deps): bump sentry-sdk from 0.16.1 to 0.16.2
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 0.16.1 to 0.16.2.
- [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.16.1...0.16.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-23 05:17:21 +00:00
3c12cf96a9 Merge pull request #137 from BeryJu/dependabot/pip/boto3-1.14.25
build(deps): bump boto3 from 1.14.24 to 1.14.25
2020-07-22 13:01:35 +02:00
d787caf0e4 build(deps): bump boto3 from 1.14.24 to 1.14.25
Bumps [boto3](https://github.com/boto/boto3) from 1.14.24 to 1.14.25.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.24...1.14.25)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-22 05:14:06 +00:00
0fc2f32d3d Merge pull request #135 from BeryJu/dependabot/npm_and_yarn/passbook/static/static/codemirror-5.56.0
build(deps): bump codemirror from 5.55.0 to 5.56.0 in /passbook/static/static
2020-07-21 10:30:50 +02:00
894d5da1d8 Merge pull request #136 from BeryJu/dependabot/pip/boto3-1.14.24
build(deps): bump boto3 from 1.14.23 to 1.14.24
2020-07-21 10:30:35 +02:00
985d20d025 build(deps): bump boto3 from 1.14.23 to 1.14.24
Bumps [boto3](https://github.com/boto/boto3) from 1.14.23 to 1.14.24.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.23...1.14.24)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-21 05:19:53 +00:00
94f3e6d0c5 build(deps): bump codemirror in /passbook/static/static
Bumps [codemirror](https://github.com/codemirror/CodeMirror) from 5.55.0 to 5.56.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.55.0...5.56.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-21 05:16:34 +00:00
0a196608c7 Merge pull request #134 from BeryJu/consent-mode
Add different Modes to Consent Stage
2020-07-20 19:14:33 +02:00
d33f0fb2cf Merge branch 'master' into consent-mode
# Conflicts:
#	passbook/stages/consent/models.py
2020-07-20 18:56:38 +02:00
ffff69ada0 stages/consent: add unittests for new modes 2020-07-20 18:47:52 +02:00
37a432267d Squashed commit of the following:
commit 88029a4335
Author: Jens Langhammer <jens.langhammer@beryju.org>
Date:   Mon Jul 20 16:55:55 2020 +0200

    admin: update to work with new form

commit 4040eb9619
Author: Jens Langhammer <jens.langhammer@beryju.org>
Date:   Mon Jul 20 16:43:30 2020 +0200

    *: remove path-based import from all PropertyMappings

commit c9663a08da
Author: Jens Langhammer <jens.langhammer@beryju.org>
Date:   Mon Jul 20 16:33:34 2020 +0200

    flows: update work with new stages

commit a3d92ebc0a
Author: Jens Langhammer <jens.langhammer@beryju.org>
Date:   Mon Jul 20 16:23:30 2020 +0200

    stages/*: remove path-based import from all stages

commit 6fa825e372
Author: Jens Langhammer <jens.langhammer@beryju.org>
Date:   Mon Jul 20 16:03:55 2020 +0200

    providers/*: remove path-based import from all providers

commit 6aefd072c8
Author: Jens Langhammer <jens.langhammer@beryju.org>
Date:   Mon Jul 20 15:58:48 2020 +0200

    policies/*: remove path-based import from all policies

commit ac2dd3611f
Author: Jens Langhammer <jens.langhammer@beryju.org>
Date:   Mon Jul 20 15:11:27 2020 +0200

    sources/*: remove path-based import from all sources

commit 74e628ce9c
Author: Jens Langhammer <jens.langhammer@beryju.org>
Date:   Mon Jul 20 14:43:38 2020 +0200

    ui: allow overriding of verbose_name

commit d4ee18ee32
Author: Jens Langhammer <jens.langhammer@beryju.org>
Date:   Mon Jul 20 14:08:27 2020 +0200

    sources/oauth: migrate from discordapp.com to discord.com
2020-07-20 18:17:14 +02:00
88029a4335 admin: update to work with new form 2020-07-20 17:57:06 +02:00
4040eb9619 *: remove path-based import from all PropertyMappings 2020-07-20 16:54:23 +02:00
c9663a08da flows: update work with new stages 2020-07-20 16:33:34 +02:00
a3d92ebc0a stages/*: remove path-based import from all stages 2020-07-20 16:28:45 +02:00
6fa825e372 providers/*: remove path-based import from all providers 2020-07-20 16:05:09 +02:00
6aefd072c8 policies/*: remove path-based import from all policies 2020-07-20 16:05:09 +02:00
ac2dd3611f sources/*: remove path-based import from all sources 2020-07-20 16:05:09 +02:00
74e628ce9c ui: allow overriding of verbose_name 2020-07-20 14:43:38 +02:00
d4ee18ee32 sources/oauth: migrate from discordapp.com to discord.com 2020-07-20 14:08:27 +02:00
9ff3ee7c0c Merge branch 'master' into consent-mode 2020-07-20 13:59:09 +02:00
418b94a45a e2e: fix grafana docker image tag 2020-07-20 13:58:50 +02:00
1393078fe6 e2e: fix oauth/oidc tests not working with current grafana 2020-07-20 13:49:07 +02:00
50612991fa stages/consent: start implementing user consent 2020-07-20 13:19:58 +02:00
37b2400cdb lib: move SAML timestring utils into lib 2020-07-20 11:35:16 +02:00
05c3393669 Merge pull request #133 from BeryJu/expiring-models
core: separate expiry logic from tokens and make re-usable
2020-07-20 11:17:37 +02:00
c60d1e1f9a core: separate expiry logic from tokens and make re-usable 2020-07-20 10:57:12 +02:00
2be7d3191f Merge pull request #132 from BeryJu/dependabot/pip/boto3-1.14.23
build(deps): bump boto3 from 1.14.22 to 1.14.23
2020-07-20 09:10:14 +02:00
aa692fdacb Merge pull request #131 from BeryJu/dependabot/npm_and_yarn/passbook/static/static/patternfly/patternfly-4.23.3
build(deps): bump @patternfly/patternfly from 4.16.7 to 4.23.3 in /passbook/static/static
2020-07-20 09:09:58 +02:00
c163637bfd build(deps): bump boto3 from 1.14.22 to 1.14.23
Bumps [boto3](https://github.com/boto/boto3) from 1.14.22 to 1.14.23.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.22...1.14.23)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-20 05:20:24 +00:00
5552aca079 build(deps): bump @patternfly/patternfly in /passbook/static/static
Bumps [@patternfly/patternfly](https://github.com/patternfly/patternfly) from 4.16.7 to 4.23.3.
- [Release notes](https://github.com/patternfly/patternfly/releases)
- [Changelog](https://github.com/patternfly/patternfly/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/patternfly/patternfly/compare/prerelease-v4.16.7...prerelease-v4.23.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-20 05:14:37 +00:00
ff2456dcfa root: clean log output, always show logger 2020-07-19 22:48:52 +02:00
539264c396 Merge pull request #130 from BeryJu/dependabot/pip/boto3-1.14.22
build(deps): bump boto3 from 1.14.21 to 1.14.22
2020-07-17 10:48:26 +02:00
1acfaf1562 build(deps): bump boto3 from 1.14.21 to 1.14.22
Bumps [boto3](https://github.com/boto/boto3) from 1.14.21 to 1.14.22.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.21...1.14.22)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-17 05:20:16 +00:00
a81e277cfa Merge pull request #128 from BeryJu/dependabot/npm_and_yarn/passbook/static/static/fortawesome/fontawesome-free-5.14.0
build(deps): bump @fortawesome/fontawesome-free from 5.13.1 to 5.14.0 in /passbook/static/static
2020-07-16 16:01:42 +02:00
b4cb78f33f Merge pull request #129 from BeryJu/dependabot/pip/boto3-1.14.21
build(deps): bump boto3 from 1.14.20 to 1.14.21
2020-07-16 16:01:20 +02:00
35c0a9532f build(deps): bump boto3 from 1.14.20 to 1.14.21
Bumps [boto3](https://github.com/boto/boto3) from 1.14.20 to 1.14.21.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.20...1.14.21)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-16 05:18:26 +00:00
aff074420b build(deps): bump @fortawesome/fontawesome-free
Bumps [@fortawesome/fontawesome-free](https://github.com/FortAwesome/Font-Awesome) from 5.13.1 to 5.14.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.13.1...5.14.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-16 05:15:24 +00:00
edbea9ccff audit: fix list not having loginrequired
closes #127
2020-07-15 18:34:53 +02:00
6b26e10ea2 new release: 0.9.0-rc1 2020-07-15 12:00:47 +02:00
a737335fdd ci: fix database connections failing 2020-07-14 22:55:30 +02:00
e15f7d7f28 flows: fix potential open redirect vuln 2020-07-14 21:57:28 +02:00
fbf9554a9e flows: fix SESSION_KEY_GET being deleted too early 2020-07-14 21:42:47 +02:00
5f34b08433 ci: fix failed tests not failing CI pipeline 2020-07-14 21:36:40 +02:00
f67a03ad66 Merge pull request #126 from BeryJu/dependabot/pip/elastic-apm-5.8.1
build(deps): bump elastic-apm from 5.8.0 to 5.8.1
2020-07-14 09:19:07 +02:00
6095301337 build(deps): bump elastic-apm from 5.8.0 to 5.8.1
Bumps [elastic-apm](https://github.com/elastic/apm-agent-python) from 5.8.0 to 5.8.1.
- [Release notes](https://github.com/elastic/apm-agent-python/releases)
- [Changelog](https://github.com/elastic/apm-agent-python/blob/master/CHANGELOG.asciidoc)
- [Commits](https://github.com/elastic/apm-agent-python/compare/v5.8.0...v5.8.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-14 05:20:24 +00:00
4a774b5885 Merge pull request #123 from BeryJu/dependabot/pip/pylint-django-2.1.0
build(deps-dev): bump pylint-django from 2.0.15 to 2.1.0
2020-07-13 16:49:33 +02:00
aa8fac3a06 Merge pull request #124 from BeryJu/dependabot/pip/django-prometheus-2.1.0.dev52
build(deps): bump django-prometheus from 2.1.0.dev46 to 2.1.0.dev52
2020-07-13 16:49:12 +02:00
b8407f5bf6 Merge pull request #125 from BeryJu/dependabot/pip/sentry-sdk-0.16.1
build(deps): bump sentry-sdk from 0.16.0 to 0.16.1
2020-07-13 16:49:02 +02:00
989c426211 Merge branch 'master' into dependabot/pip/pylint-django-2.1.0 2020-07-13 16:16:16 +02:00
9a888cfcf1 Merge branch 'master' into dependabot/pip/django-prometheus-2.1.0.dev52 2020-07-13 16:16:08 +02:00
72ec871729 Merge branch 'master' into dependabot/pip/sentry-sdk-0.16.1 2020-07-13 16:15:58 +02:00
8d58842c9b e2e: decrease timeouts to fix failed tests 2020-07-13 08:54:56 +02:00
a90aa5e069 build(deps): bump sentry-sdk from 0.16.0 to 0.16.1
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 0.16.0 to 0.16.1.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGES.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/0.16.0...0.16.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-13 05:20:41 +00:00
639020a2e1 build(deps): bump django-prometheus from 2.1.0.dev46 to 2.1.0.dev52
Bumps [django-prometheus](https://github.com/korfuri/django-prometheus) from 2.1.0.dev46 to 2.1.0.dev52.
- [Release notes](https://github.com/korfuri/django-prometheus/releases)
- [Changelog](https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/korfuri/django-prometheus/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-13 05:18:58 +00:00
8e6f915ec6 build(deps-dev): bump pylint-django from 2.0.15 to 2.1.0
Bumps [pylint-django](https://github.com/PyCQA/pylint-django) from 2.0.15 to 2.1.0.
- [Release notes](https://github.com/PyCQA/pylint-django/releases)
- [Changelog](https://github.com/PyCQA/pylint-django/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/PyCQA/pylint-django/compare/v2.0.15...v2.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-13 05:17:14 +00:00
6631471566 root: update version in readme 2020-07-12 22:53:25 +02:00
b452e751ea flows: add SESSION_KEY_APPLICATION_PRE
whenever a user tries to access an application without being authenticated to passbook, we now show notice which application they are going to continue to.
2020-07-12 22:47:46 +02:00
a3baa100d4 sources/saml: remove unused import 2020-07-12 18:55:26 +02:00
f7b9de1261 */saml: fix MetadataProcessor having generic namespace prefixes 2020-07-12 18:40:43 +02:00
47ca566d06 sources/saml: fix MetadataProcessor not working, add unittests 2020-07-12 18:40:18 +02:00
a943d060d2 core: add separate autosubmit form for use without flows 2020-07-12 18:24:36 +02:00
1675dab314 providers/saml: fix encoding for POST bindings 2020-07-12 17:58:38 +02:00
996aa367d3 core: fix autosubmit_form loading full template 2020-07-12 17:45:03 +02:00
be6f342e58 providers/saml: fix RelayState being included when None given 2020-07-12 17:22:14 +02:00
464b558a02 */saml: fix typo 2020-07-12 17:20:41 +02:00
d1151091cd providers/saml: Generate NameID Value based on NameID Policy received 2020-07-12 17:06:35 +02:00
f8e5383ba2 providers/saml: parse NameID Policy from AuthnRequest 2020-07-12 17:05:48 +02:00
06f73512df lib/evaluator: add support for IP Address comparison 2020-07-12 16:36:49 +02:00
0ff4545bab providers/saml: fix AuthnRequest Signature validation, add unittests 2020-07-12 16:17:53 +02:00
ff6e270886 sources/saml: fix AuthnRequest Singing for redirect bindings 2020-07-12 16:17:35 +02:00
8aa0b72b67 e2e: only save screenshots in CI 2020-07-12 16:17:04 +02:00
91766a2162 sources/saml: automatically add RelayState to build_auth_n_detached 2020-07-12 01:46:46 +02:00
a393097504 */saml: start implementing unittests, fix signing 2020-07-12 01:44:34 +02:00
2056b86ce7 providers/saml: rewrite SAML AuthNRequest Parser and Response Processor 2020-07-11 14:06:42 +02:00
1b0c013d8e providers/saml: remove processor_path field 2020-07-11 13:28:10 +02:00
92a09be8c0 sources/saml: rewrite Processors and Views to directly build XML without templates 2020-07-11 01:02:55 +02:00
1e31cd03ed build(deps): bump lxml from 4.5.1 to 4.5.2 (#121)
Bumps [lxml](https://github.com/lxml/lxml) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.5.1...lxml-4.5.2)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Jens L <jens@beryju.org>
2020-07-10 23:31:13 +02:00
dc863a6e87 build(deps): bump boto3 from 1.14.19 to 1.14.20 (#122)
Bumps [boto3](https://github.com/boto/boto3) from 1.14.19 to 1.14.20.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.19...1.14.20)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Jens L <jens@beryju.org>
2020-07-10 22:44:17 +02:00
d74366f413 policies/hibp: update for flows, add unittests 2020-07-10 20:57:15 +02:00
5bcf2aef8c policies/password: Add Password Policy tests, update password policy for flows 2020-07-10 20:53:08 +02:00
8de3c4fbd6 sources/ldap: improve unittests 2020-07-10 20:21:51 +02:00
c191b62245 ci: attempt to fix Coverage not being registered 2020-07-10 19:35:19 +02:00
0babbde00e ci: fix test results not being merged correctly 2020-07-10 19:11:36 +02:00
b8af312ab1 ci: fix artifacts being downloaded into wrong directory 2020-07-10 18:39:16 +02:00
38cabfb325 ci: fix wrong coverage command being executed 2020-07-10 18:07:18 +02:00
0a3528b5f4 ci: fix targetPath and artifact being swapped 2020-07-10 17:43:16 +02:00
30a672758a ci: fix Stage names 2020-07-10 17:25:19 +02:00
723a825085 ci: separate unittests and e2e into separate runs, combine afterwards 2020-07-10 17:12:37 +02:00
40e794099a e2e: only initialise selenium after setting up container 2020-07-10 16:49:25 +02:00
111b037512 e2e: use non-debug selenium docker image for CI 2020-07-10 15:37:10 +02:00
52f66717d3 e2e: print screenshot filename after test 2020-07-10 15:28:01 +02:00
7ac4242a38 e2e: add test for OAuth Enrollment -> OAuth Authentication 2020-07-10 00:14:48 +02:00
4caa4be476 sources/oauth: fix UserOAuthSourceConnection not being assigned to user after enrollment
sources/oauth: separate handle_new_connection into handle_existing_user_link and handle_enroll
2020-07-10 00:07:59 +02:00
c6d8bae147 e2e: generate dex config dynamically 2020-07-09 23:15:22 +02:00
c70310730a sources/oauth: split up single large "core" views 2020-07-09 23:09:32 +02:00
2d2b2d08f4 core: fix source slug not being unique 2020-07-09 23:05:46 +02:00
8fe6a5b62d stages/prompt: fix checkbox not working, fix date and datetime not using HTML5 input types 2020-07-09 22:55:44 +02:00
5e6221deb8 Merge pull request #120 from BeryJu/dependabot/pip/boto3-1.14.19
build(deps): bump boto3 from 1.14.18 to 1.14.19
2020-07-09 22:55:21 +02:00
c3b493f7d4 Merge branch 'master' into dependabot/pip/boto3-1.14.19 2020-07-09 16:55:22 +02:00
dbcb5b4f63 e2e: remove static oauth secret 2020-07-09 14:59:25 +02:00
f0640fcea9 build(deps): bump boto3 from 1.14.18 to 1.14.19
Bumps [boto3](https://github.com/boto/boto3) from 1.14.18 to 1.14.19.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.18...1.14.19)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-09 05:19:44 +00:00
64c47a59f8 e2e: add tests for OAuth Source, update tests for new base templates 2020-07-09 00:53:18 +02:00
3450b8f1fe docs: update screenshots 2020-07-09 00:26:18 +02:00
9518cefdd7 flows: fix default-source-enrollment-if-username expression 2020-07-09 00:20:42 +02:00
32d5c26577 core: fix base_full template missing messages 2020-07-09 00:18:45 +02:00
ef2cdf27b3 stages/prompt: add static and separator elements 2020-07-09 00:00:15 +02:00
e58ac7ae90 polices: add helper to remove None-value keys from dict for policies 2020-07-08 23:07:16 +02:00
d786fa4b7c sources/oauth: rewrite to not directly create user, pre-seed data into flow 2020-07-08 20:39:20 +02:00
0e3e73989d sources/saml: Add NameID Policy field, sent with AuthnRequest 2020-07-08 16:18:09 +02:00
d831599608 core: make autosubmit_form generic template 2020-07-08 14:27:58 +02:00
1e57926603 sources/saml: add POST_AUTO binding which auto redirects to IdP 2020-07-08 14:18:08 +02:00
1524880eec core: add generic login/base_full template for static login views 2020-07-08 14:17:29 +02:00
0bfb623f97 providers/saml: fix autosubmit_form using wrong template 2020-07-08 14:12:44 +02:00
429627494c root: fix passbook.footer_links not being rendered 2020-07-08 13:18:33 +02:00
9feea155fe root: fix /favicon being routed to application server 2020-07-08 13:18:08 +02:00
2717e02d93 Merge pull request #119 from BeryJu/dependabot/pip/boto3-1.14.18
build(deps): bump boto3 from 1.14.17 to 1.14.18
2020-07-08 12:57:40 +02:00
18bd803b0d build(deps): bump boto3 from 1.14.17 to 1.14.18
Bumps [boto3](https://github.com/boto/boto3) from 1.14.17 to 1.14.18.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.17...1.14.18)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-08 05:16:20 +00:00
c7f078ffcc new release: 0.9.0-pre7 2020-07-07 22:34:44 +02:00
571cb3d65f sources/oauth: disable twitter source while its broken 2020-07-07 22:25:50 +02:00
8c500c38b1 policies/reputation: only change score when credentials contain username 2020-07-07 22:25:37 +02:00
5644e57e6a sources/oauth: directly call AuthorizedServiceBackend instead of authenticate() 2020-07-07 22:23:45 +02:00
cfc181eed1 sources/oauth: fix wrong comparions
closes #118
2020-07-07 21:46:16 +02:00
91bea38b8e lib: ignore APM errors 2020-07-07 21:45:36 +02:00
d95c5aa739 root: allow changing of APM verify_server_cert setting 2020-07-07 19:59:32 +02:00
0b250b897e new release: 0.9.0-pre6 2020-07-07 19:14:29 +02:00
c6880a0f16 Merge pull request #117 from BeryJu/apm
Support for Elastic APM
2020-07-07 18:48:40 +02:00
beb5ffcbdd ci: fix gatekeeper dockerfile path 2020-07-07 18:48:24 +02:00
0715cac39b root: remove psutil as we have external monitoring for CPU 2020-07-07 18:24:24 +02:00
41117d873d ci: fix gatekeeper building the wrong image 2020-07-07 18:23:15 +02:00
231e448b1a lib/eval: fix import order 2020-07-07 18:05:38 +02:00
b3b8cd807d root: expose APM settings in helm chart 2020-07-07 17:54:07 +02:00
9021bbd5de root: implement APM support 2020-07-07 17:43:10 +02:00
169475ab39 crypto: add colon seperator for fingerprint 2020-07-07 17:05:31 +02:00
c00e01626e sources/ldap: adjust task schedule name 2020-07-07 17:04:07 +02:00
05d4a9ef62 policies/reputation: rewrite to save score into cache and save into DB via worker 2020-07-07 17:03:57 +02:00
17a2ac73e7 stages/user_write: add signals 2020-07-07 15:49:02 +02:00
6bc6f947dd stages/invitation: move invite signals from core to app 2020-07-07 15:46:13 +02:00
b048a1fb4f ci: notify sentry of new releases 2020-07-07 14:09:28 +02:00
363940ee8d root: fix API requests erroring 2020-07-07 14:02:20 +02:00
a64e53479c Merge pull request #115 from BeryJu/dependabot/pip/boto3-1.14.17
build(deps): bump boto3 from 1.14.16 to 1.14.17
2020-07-07 13:34:53 +02:00
14fdbe7720 Merge pull request #116 from BeryJu/dependabot/pip/coverage-5.2
build(deps-dev): bump coverage from 5.1 to 5.2
2020-07-07 13:34:41 +02:00
f56332c954 Merge branch 'master' into dependabot/pip/boto3-1.14.17 2020-07-07 13:14:07 +02:00
21c53c748f Merge branch 'master' into dependabot/pip/coverage-5.2 2020-07-07 13:13:55 +02:00
b12182c1d1 admin: improve overview layout 2020-07-07 13:13:15 +02:00
d8f27f595a admin: use django cache for admin version (expiry) 2020-07-07 13:12:54 +02:00
b25dc2aaa3 build(deps-dev): bump coverage from 5.1 to 5.2
Bumps [coverage](https://github.com/nedbat/coveragepy) from 5.1 to 5.2.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/coverage-5.1...coverage-5.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-07 05:24:51 +00:00
3ec3849e72 build(deps): bump boto3 from 1.14.16 to 1.14.17
Bumps [boto3](https://github.com/boto/boto3) from 1.14.16 to 1.14.17.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.16...1.14.17)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-07 05:23:18 +00:00
2dc1b65718 ui: fix modal layout 2020-07-06 20:50:14 +02:00
af22f507f4 sources/oauth: fix template for user settings 2020-07-06 17:48:53 +02:00
9958019bf3 core: fix user's sidebar links for sources 2020-07-06 17:46:41 +02:00
02d65972cb admin: fix submit button on update form 2020-07-06 17:46:30 +02:00
24ad893350 admin: fix token_list template 2020-07-06 17:43:20 +02:00
9c5792b1e1 docs: migrate TOTP and Static OTP devices 2020-07-06 17:42:46 +02:00
094d191bff new release: 0.9.0-pre5 2020-07-06 12:52:34 +02:00
49fb9f688b Merge pull request #114 from BeryJu/dependabot/pip/sentry-sdk-0.16.0
build(deps): bump sentry-sdk from 0.15.1 to 0.16.0
2020-07-06 11:51:21 +02:00
7d161e5aa1 build(deps): bump sentry-sdk from 0.15.1 to 0.16.0
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 0.15.1 to 0.16.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.15.1...0.16.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-06 05:18:21 +00:00
78e5d471e3 core: fix type annotation for user settings 2020-07-05 23:49:33 +02:00
2e2c9f5287 api: add token authentication 2020-07-05 23:37:58 +02:00
d5a3e09a98 core: add token Intents 2020-07-05 23:14:57 +02:00
2402cfe29d providers/* use name for __str__ 2020-07-05 23:00:40 +02:00
26613b6ea9 core: fix application overview 2020-07-05 22:58:52 +02:00
e5165abf04 stages/user_login: Allow changing of session duration 2020-07-04 15:20:45 +02:00
b26882a450 flows: FlowStageBinding group Stage by type 2020-07-04 15:02:21 +02:00
94281bee88 admin: improve policy binding listing by showing Target object type 2020-07-04 00:18:19 +02:00
16b966c16e policies: Show grouped Dropdown for Target 2020-07-04 00:16:16 +02:00
d3b0992456 flows: FlowStageBinding: rename .flow to .target to fix select_subclasses() 2020-07-04 00:14:21 +02:00
dd74b73b4f Merge pull request #40 from BeryJu/azure-pipelines
Set up CI with Azure Pipelines
2020-07-03 10:47:29 +02:00
0bdfccc1f3 ci: final cleanup 2020-07-03 10:17:24 +02:00
ceb0793bc9 ci: publish unittest results and coverage 2020-07-03 09:54:25 +02:00
abea85b635 ci: fix incorrect node version for pyright 2020-07-03 09:39:23 +02:00
01c83f6f4a Merge branch 'master' into azure-pipelines
# Conflicts:
#	.github/workflows/ci.yml
#	README.md
2020-07-03 09:33:04 +02:00
9167c9c3ba Merge pull request #112 from BeryJu/dependabot/pip/django-prometheus-2.1.0.dev46
build(deps): bump django-prometheus from 2.1.0.dev42 to 2.1.0.dev46
2020-07-03 09:09:16 +02:00
04add2e52d build(deps): bump django-prometheus from 2.1.0.dev42 to 2.1.0.dev46
Bumps [django-prometheus](https://github.com/korfuri/django-prometheus) from 2.1.0.dev42 to 2.1.0.dev46.
- [Release notes](https://github.com/korfuri/django-prometheus/releases)
- [Changelog](https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/korfuri/django-prometheus/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-03 06:53:47 +00:00
1e9241d45b Merge pull request #111 from BeryJu/dependabot/pip/boto3-1.14.16
build(deps): bump boto3 from 1.14.15 to 1.14.16
2020-07-03 08:49:41 +02:00
22ee198a31 build(deps): bump boto3 from 1.14.15 to 1.14.16
Bumps [boto3](https://github.com/boto/boto3) from 1.14.15 to 1.14.16.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.15...1.14.16)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-03 05:20:53 +00:00
1d9c92d548 admin: add generic form tests 2020-07-02 22:29:30 +02:00
b30b58924f e2e: Add denied tests for oauth and oidc provider 2020-07-02 21:55:02 +02:00
bead19c64c flows: cleanup denied view, use everywhere 2020-07-02 13:48:42 +02:00
76e2ba4764 e2e/provider/saml: add negative case 2020-07-02 13:48:21 +02:00
8d095d7436 Merge pull request #109 from BeryJu/dependabot/pip/django-prometheus-2.1.0.dev42
build(deps): bump django-prometheus from 2.1.0.dev40 to 2.1.0.dev42
2020-07-02 11:59:11 +02:00
d3a7fd5818 build(deps): bump django-prometheus from 2.1.0.dev40 to 2.1.0.dev42
Bumps [django-prometheus](https://github.com/korfuri/django-prometheus) from 2.1.0.dev40 to 2.1.0.dev42.
- [Release notes](https://github.com/korfuri/django-prometheus/releases)
- [Changelog](https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/korfuri/django-prometheus/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-02 09:56:06 +00:00
247a8dbc8f Merge pull request #110 from BeryJu/dependabot/pip/boto3-1.14.15
build(deps): bump boto3 from 1.14.14 to 1.14.15
2020-07-02 11:52:16 +02:00
9241adfc68 build(deps): bump boto3 from 1.14.14 to 1.14.15
Bumps [boto3](https://github.com/boto/boto3) from 1.14.14 to 1.14.15.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.14...1.14.15)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-02 05:21:07 +00:00
ae83ee6d31 providers/saml: fix access result not being checked properly 2020-07-02 00:23:52 +02:00
4701374021 admin: remove duplicate code into new base classes 2020-07-02 00:13:33 +02:00
bd40585247 providers/samlv2: remove SAMLv2 from master 2020-07-01 23:21:58 +02:00
cc0b8164b0 providers/*: use PolicyAccessMixin to simplify 2020-07-01 23:18:10 +02:00
310b31a8b7 core: fix linting 2020-07-01 22:35:38 +02:00
13900bc603 lib: cleanup unused widgets 2020-07-01 22:27:58 +02:00
6634cc2edf root: add group_membership policy 2020-07-01 21:18:05 +02:00
3478a2cf6d admin: add filter to hide classes with __debug_only__ when Debug is disabled 2020-07-01 18:53:13 +02:00
3b70d12a5f *: rephrase strings 2020-07-01 18:40:52 +02:00
219acf76d5 core: fix forms for radio buttons 2020-07-01 12:47:27 +02:00
ec6f467fa2 ui: Make Checkbox label click trigger checkbox toggle 2020-07-01 12:37:13 +02:00
0e6561987e admin: fix user and group create not triggering sidebar 2020-07-01 12:36:44 +02:00
62c20b6e67 admin: add list of all tokens 2020-07-01 12:27:30 +02:00
13084562c5 admin: fix Password Recovery function not working 2020-07-01 12:10:12 +02:00
02c1c434a2 core: update styling of impersonate banner 2020-07-01 12:01:58 +02:00
5f04a75878 new release: 0.9.0-pre4 2020-07-01 09:50:21 +02:00
3556c76674 Merge pull request #107 from BeryJu/dependabot/pip/django-3.0.8
build(deps): bump django from 3.0.7 to 3.0.8
2020-07-01 09:21:15 +02:00
c7d638de2f build(deps): bump django from 3.0.7 to 3.0.8
Bumps [django](https://github.com/django/django) from 3.0.7 to 3.0.8.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.0.7...3.0.8)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-01 07:07:06 +00:00
143733499f Merge pull request #108 from BeryJu/dependabot/pip/docker-4.2.2
build(deps-dev): bump docker from 4.2.1 to 4.2.2
2020-07-01 09:03:38 +02:00
0d6a0ffe14 build(deps-dev): bump docker from 4.2.1 to 4.2.2
Bumps [docker](https://github.com/docker/docker-py) from 4.2.1 to 4.2.2.
- [Release notes](https://github.com/docker/docker-py/releases)
- [Commits](https://github.com/docker/docker-py/compare/4.2.1...4.2.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-01 06:41:52 +00:00
6d4c7312d8 Merge pull request #106 from BeryJu/dependabot/pip/boto3-1.14.14
build(deps): bump boto3 from 1.14.13 to 1.14.14
2020-07-01 08:37:37 +02:00
2cb6a179e8 build(deps): bump boto3 from 1.14.13 to 1.14.14
Bumps [boto3](https://github.com/boto/boto3) from 1.14.13 to 1.14.14.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.13...1.14.14)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-01 05:21:37 +00:00
7de2ad77b5 Merge pull request #101 from BeryJu/otp-rework
OTP Stage Rework
2020-06-30 22:03:28 +02:00
89c33060d4 core: fix user settings sidebar buttons not being enabled 2020-06-30 21:26:43 +02:00
b61f595562 core: ensure user settings are sorted by name 2020-06-30 21:24:58 +02:00
ce2230f774 stages/otp_*: update user setting design 2020-06-30 21:23:37 +02:00
d18a78d04d Merge branch 'master' into otp-rework 2020-06-30 20:44:39 +02:00
c59c6aa728 stages/*: minor UI updates, cleanup 2020-06-30 19:06:41 +02:00
729910c383 stages/*: minor string updates 2020-06-30 18:52:18 +02:00
37fe637422 stages/password: make template inherit form_with_user 2020-06-30 18:50:24 +02:00
3114d064ed e2e: improve error message when using wait_for_url 2020-06-30 16:36:30 +02:00
2ca5e1eedb stages/otp_*: fix linting 2020-06-30 16:12:19 +02:00
d2bf579ff6 stages/otp_static: start implementing static stage 2020-06-30 15:44:34 +02:00
3716bda76e stages/otp_time: fix linting 2020-06-30 13:44:09 +02:00
a76eb4d30f stages/otp_time: Cleanup, use django_otp's URL generator 2020-06-30 12:42:39 +02:00
7c191b0984 stages/otp_validate: Implement OTP Validation stage 2020-06-30 12:42:12 +02:00
9613fcde89 Squashed commit of the following:
commit 885a2ed057
Author: Jens Langhammer <jens.langhammer@beryju.org>
Date:   Tue Jun 30 12:17:23 2020 +0200

    flows: fix linting error
2020-06-30 12:18:01 +02:00
885a2ed057 flows: fix linting error 2020-06-30 12:17:23 +02:00
b270fb0742 stages/otp_time: implement TOTP Setup stage 2020-06-30 12:14:40 +02:00
285a69d91f Merge branch 'master' into otp-rework 2020-06-30 11:23:09 +02:00
de3b753a26 flows: show error message in flow when stage raises 2020-06-30 11:18:39 +02:00
34be1dd9f4 admin: add execute button to flow which executes flow without cache 2020-06-30 11:18:20 +02:00
a4c0fb9e75 otp_time: fix linting 2020-06-30 10:42:27 +02:00
f040223646 audit: move events list from admin to audit app 2020-06-30 10:23:54 +02:00
bf297b8593 admin: add info about latest version 2020-06-30 10:23:39 +02:00
43eea9e99c Merge pull request #105 from BeryJu/dependabot/pip/boto3-1.14.13
build(deps): bump boto3 from 1.14.12 to 1.14.13
2020-06-30 09:42:08 +02:00
8e38bc87bc build(deps): bump boto3 from 1.14.12 to 1.14.13
Bumps [boto3](https://github.com/boto/boto3) from 1.14.12 to 1.14.13.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.12...1.14.13)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-30 05:16:31 +00:00
50a57fb3dd Merge pull request #104 from BeryJu/flows-cancel
flows: add CancelView to cancel current flow execution
2020-06-30 01:27:12 +02:00
38b8bc182f Merge branch 'master' into flows-cancel 2020-06-30 00:47:18 +02:00
9743ad33d6 ci: add snyk 2020-06-30 00:45:04 +02:00
b746ce97ba Merge branch 'master' into flows-cancel 2020-06-30 00:20:33 +02:00
dbee714dac api: fix consent stage missing from API 2020-06-30 00:19:06 +02:00
d33f632203 flows: add CancelView to cancel current flow execution 2020-06-30 00:11:01 +02:00
812aa4ced5 Merge branch 'master' into otp-rework 2020-06-29 23:32:36 +02:00
63466e3384 e2e: wait for grafana URL 2020-06-29 23:04:05 +02:00
920858ff72 Merge branch 'master' into otp-rework
# Conflicts:
#	passbook/flows/models.py
#	passbook/stages/otp/models.py
#	swagger.yaml
2020-06-29 22:54:18 +02:00
56f599e4aa Merge pull request #103 from BeryJu/stage-password-change
stages/password: Password Change
2020-06-29 22:29:20 +02:00
05183ed937 e2e: saml provider: wait for URL 2020-06-29 22:11:50 +02:00
8d31eef47d stages/password: assign default password change flow to password stage
add e2e tests for password change flow
2020-06-29 21:47:20 +02:00
96a6ac85df audit: add cleanse_dict function to ensure no passwords end in logs 2020-06-29 19:13:07 +02:00
5a60341a6e build(deps): bump boto3 from 1.14.11 to 1.14.12
Bumps [boto3](https://github.com/boto/boto3) from 1.14.11 to 1.14.12.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.11...1.14.12)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-29 16:26:53 +02:00
21ba969072 stages/password: create default password change flow 2020-06-29 16:26:21 +02:00
d6a8d8292d core: UIUserSettings: remove icon, rename view_name to URL for complete URL 2020-06-29 16:20:33 +02:00
693a92ada5 audit: fix sanitize_dict updating source dict 2020-06-29 16:19:56 +02:00
ec823aebed flows: update migrations to use update_or_create 2020-06-29 16:19:39 +02:00
b8654c06bf flows: remove generic "password change" designation and add setup_stage 2020-06-29 11:12:51 +02:00
9d03c4c7d2 flows: Stage ui_user_settings -> staticmethod with context as argument 2020-06-28 10:31:26 +02:00
8c36ab89e8 stages/otp: start separation into 3 stages, otp_time, otp_static and otp_validate 2020-06-28 10:30:35 +02:00
e75e71a5ce build(deps): bump swagger-spec-validator from 2.7.2 to 2.7.3 (#100)
Bumps [swagger-spec-validator](https://github.com/Yelp/swagger_spec_validator) from 2.7.2 to 2.7.3.
- [Release notes](https://github.com/Yelp/swagger_spec_validator/releases)
- [Changelog](https://github.com/Yelp/swagger_spec_validator/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/Yelp/swagger_spec_validator/compare/v2.7.2...v2.7.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-06-27 10:26:08 +02:00
bf008e368e build(deps): bump boto3 from 1.14.10 to 1.14.11 (#99)
Bumps [boto3](https://github.com/boto/boto3) from 1.14.10 to 1.14.11.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.10...1.14.11)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-27 10:25:47 +02:00
3c1d02bfc4 e2e: bump selenium waits even more 2020-06-26 23:38:00 +02:00
c1b2093cf7 e2e: add utility to wait for URL 2020-06-26 16:21:59 +02:00
cc7e4ad0e2 e2e: show browser logs to debug CI issues 2020-06-26 15:06:46 +02:00
c07bd6e733 ci: up node version for pyright 2020-06-26 10:31:06 +02:00
9882342ed1 Merge branch 'master' into azure-pipelines
# Conflicts:
#	.github/workflows/ci.yml
2020-06-02 20:40:04 +02:00
1c906b12be ci: set static network for static build 2020-05-29 10:04:23 +02:00
4d835b18cc ci: fix network for static build 2020-05-29 09:43:00 +02:00
e02ff7ec30 ci: fix codecov token not being set correctly 2020-05-29 09:18:17 +02:00
2e67b0194b Update azure-pipelines.yml for Azure Pipelines 2020-05-29 09:15:57 +02:00
02f0712934 ci: fix static being built on wrong docker image 2020-05-28 21:19:06 +02:00
7e7ea47f39 ci: fix level of stages on build jobs 2020-05-28 21:00:30 +02:00
7e52711e3a ci: fix names of build jobs 2020-05-28 19:46:10 +02:00
40fd1c9c1f ci: fix duplicate key 2020-05-28 19:45:25 +02:00
4037a444eb ci: migrate building 2020-05-28 19:44:25 +02:00
1ed7e900f2 ci: migrate unittests and coverage 2020-05-28 19:29:28 +02:00
cfc8d0a0f7 ci: migrate lint to az 2020-05-28 19:15:18 +02:00
df33616544 Set up CI with Azure Pipelines
[skip ci]
2020-05-28 18:57:48 +02:00
382 changed files with 8208 additions and 5080 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.9.0-pre3
current_version = 0.9.0-stable
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -15,6 +15,10 @@ values =
beta
stable
[bumpversion:file:README.md]
[bumpversion:file:docs/installation/docker-compose.md]
[bumpversion:file:helm/values.yaml]
[bumpversion:file:helm/Chart.yaml]

View File

@ -5,8 +5,6 @@ omit =
manage.py
*/migrations/*
*/apps.py
passbook/management/commands/web.py
passbook/management/commands/worker.py
docs/
[report]

View File

@ -1,220 +0,0 @@
name: passbook-ci
on:
- push
env:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
jobs:
# Linting
pylint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- name: Install dependencies
run: sudo pip install -U wheel pipenv && pipenv install --dev
- name: Lint with pylint
run: pipenv run pylint passbook
black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- name: Install dependencies
run: sudo pip install -U wheel pipenv && pipenv install --dev
- name: Lint with black
run: pipenv run black --check passbook
prospector:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- name: Install dependencies
run: sudo pip install -U wheel pipenv && pipenv install --dev && pipenv install --dev prospector --skip-lock
- name: Lint with prospector
run: pipenv run prospector
bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- name: Install dependencies
run: sudo pip install -U wheel pipenv && pipenv install --dev
- name: Lint with bandit
run: pipenv run bandit -r passbook
pyright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- name: Install pyright
run: npm install -g pyright
- name: Show pyright version
run: pyright --version
- name: Install dependencies
run: sudo pip install -U wheel pipenv && pipenv install --dev
- name: Lint with pyright
run: pipenv run pyright
# Actual CI tests
migrations:
needs:
- pylint
- black
- prospector
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
ports:
- 5432:5432
redis:
image: redis:latest
ports:
- 6379:6379
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- name: Install dependencies
run: sudo pip install -U wheel pipenv && pipenv install --dev
- name: Run migrations
run: pipenv run ./manage.py migrate
coverage:
needs:
- pylint
- black
- prospector
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
ports:
- 5432:5432
redis:
image: redis:latest
ports:
- 6379:6379
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- uses: actions/setup-node@v1
with:
node-version: '12'
- name: Install dependencies
run: |
sudo pip install -U wheel pipenv
pipenv install --dev
- name: Prepare Chrome node
run: |
cd e2e
docker-compose pull -q chrome
docker-compose up -d chrome
- name: Build static files for e2e test
run: |
cd passbook/static/static
yarn
- name: Run coverage
run: pipenv run coverage run ./manage.py test --failfast
- uses: actions/upload-artifact@v2
if: failure()
with:
path: out/
- name: Create XML Report
run: pipenv run coverage xml
- uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
# Build
build-server:
needs:
- migrations
- coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Docker Login Registry
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
run: docker build
--no-cache
-t beryju/passbook:gh-${GITHUB_REF##*/}
-f Dockerfile .
- name: Push Docker Container to Registry
run: docker push beryju/passbook:gh-${GITHUB_REF##*/}
build-gatekeeper:
needs:
- migrations
- coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Docker Login Registry
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
run: |
cd gatekeeper
docker build \
--no-cache \
-t beryju/passbook-gatekeeper:gh-${GITHUB_REF##*/} \
-f Dockerfile .
- name: Push Docker Container to Registry
run: docker push beryju/passbook-gatekeeper:gh-${GITHUB_REF##*/}
build-static:
needs:
- migrations
- coverage
runs-on: ubuntu-latest
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
redis:
image: redis:latest
steps:
- uses: actions/checkout@v1
- name: Docker Login Registry
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
run: docker build
--no-cache
--network=$(docker network ls | grep github | awk '{print $1}')
-t beryju/passbook-static:gh-${GITHUB_REF##*/}
-f static.Dockerfile .
- name: Push Docker Container to Registry
run: docker push beryju/passbook-static:gh-${GITHUB_REF##*/}

54
.github/workflows/codeql-analysis.yml vendored Normal file
View File

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

View File

@ -16,11 +16,11 @@ jobs:
- name: Building Docker Image
run: docker build
--no-cache
-t beryju/passbook:0.9.0-pre3
-t beryju/passbook:0.9.0-stable
-t beryju/passbook:latest
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook:0.9.0-pre3
run: docker push beryju/passbook:0.9.0-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook:latest
build-gatekeeper:
@ -37,11 +37,11 @@ jobs:
cd gatekeeper
docker build \
--no-cache \
-t beryju/passbook-gatekeeper:0.9.0-pre3 \
-t beryju/passbook-gatekeeper:0.9.0-stable \
-t beryju/passbook-gatekeeper:latest \
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-gatekeeper:0.9.0-pre3
run: docker push beryju/passbook-gatekeeper:0.9.0-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-gatekeeper:latest
build-static:
@ -66,11 +66,11 @@ jobs:
run: docker build
--no-cache
--network=$(docker network ls | grep github | awk '{print $1}')
-t beryju/passbook-static:0.9.0-pre3
-t beryju/passbook-static:0.9.0-stable
-t beryju/passbook-static:latest
-f static.Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-static:0.9.0-pre3
run: docker push beryju/passbook-static:0.9.0-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-static:latest
test-release:
@ -86,3 +86,19 @@ jobs:
docker-compose up --no-start
docker-compose start postgresql redis
docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"
sentry-release:
needs:
- test-release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Create a Sentry.io release
uses: tclindner/sentry-releases-action@v1.2.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: beryjuorg
SENTRY_PROJECT: passbook
SENTRY_URL: https://sentry.beryju.org
with:
tagName: 0.9.0-stable
environment: production

3
.gitignore vendored
View File

@ -196,3 +196,6 @@ local.env.yml
### Helm ###
# Chart dependencies
**/charts/*.tgz
# Selenium Screenshots
selenium_screenshots/**

View File

@ -41,6 +41,7 @@ structlog = "*"
swagger-spec-validator = "*"
urllib3 = {extras = ["secure"],version = "*"}
facebook-sdk = "*"
elastic-apm = "*"
[requires]
python_version = "3.8"

432
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "fd0192b73c01aaffb90716ce7b6d4e5be9adb8788d3ebd58e54ccd6f85d9b71b"
"sha256": "5c22d3a514247b663a07c6492cea09ab140346894a528db06bd805a4a3a4a320"
},
"pipfile-spec": 6,
"requires": {
@ -18,10 +18,10 @@
"default": {
"amqp": {
"hashes": [
"sha256:24dbaff8ce4f30566bb88976b398e8c4e77637171af3af6f1b9650f48890e60b",
"sha256:bb68f8d2bced8f93ccfd07d96c689b716b3227720add971be980accfc2952139"
"sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21",
"sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"
],
"version": "==2.6.0"
"version": "==2.6.1"
},
"asgiref": {
"hashes": [
@ -46,18 +46,18 @@
},
"boto3": {
"hashes": [
"sha256:16f83ca3aa98d3faeb4f0738b878525770323e5fb9952435ddf58ca09aacec7c",
"sha256:dc87ef82c81d2938f91c7ebfa85dfd032fff1bd3b67c9f66d74b21f8ec1e353d"
"sha256:35553b05b47fb6b3494bc447428342ca840348ede485e586d002399a32cae0a3",
"sha256:e47537d530d523855e52367c2ff278c732651934cd6b33daf9487649dab8e674"
],
"index": "pypi",
"version": "==1.14.10"
"version": "==1.14.33"
},
"botocore": {
"hashes": [
"sha256:b22db58da273b77529edef71425f9c281bc627b1b889f81960750507238abbb8",
"sha256:cb0d7511a68439bf6f16683489130e06c5bbf9f5a9d647e0cbf63d79f3d3bdaa"
"sha256:273dbd8e26d4faa568e4cd4ca3180890b59ff0e3e8df7fb352796796c6808527",
"sha256:c12a0dc7021fca9d11c2bdbafdc44372e38180b56a1fab97c27b152f79455cd1"
],
"version": "==1.17.10"
"version": "==1.17.33"
},
"celery": {
"hashes": [
@ -76,36 +76,36 @@
},
"cffi": {
"hashes": [
"sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
"sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
"sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
"sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
"sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
"sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
"sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
"sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
"sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
"sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
"sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
"sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
"sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
"sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
"sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
"sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
"sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
"sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
"sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
"sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
"sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
"sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
"sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
"sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
"sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
"sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
"sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
"sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
"sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc",
"sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9",
"sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792",
"sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2",
"sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022",
"sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8",
"sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96",
"sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2",
"sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995",
"sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1",
"sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849",
"sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c",
"sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe",
"sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3",
"sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90",
"sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f",
"sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1",
"sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf",
"sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa",
"sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc",
"sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939",
"sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e",
"sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0",
"sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9",
"sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168",
"sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33",
"sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f",
"sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"
],
"version": "==1.14.0"
"version": "==1.14.1"
},
"chardet": {
"hashes": [
@ -162,11 +162,11 @@
},
"django": {
"hashes": [
"sha256:5052b34b34b3425233c682e0e11d658fd6efd587d11335a0203d827224ada8f2",
"sha256:e1630333248c9b3d4e38f02093a26f1e07b271ca896d73097457996e0fae12e8"
"sha256:31a5fbbea5fc71c99e288ec0b2f00302a0a92c44b13ede80b73a6a4d6d205582",
"sha256:5457fc953ec560c5521b41fad9e6734a4668b7ba205832191bbdff40ec61073c"
],
"index": "pypi",
"version": "==3.0.7"
"version": "==3.0.8"
},
"django-cors-middleware": {
"hashes": [
@ -232,11 +232,11 @@
},
"django-prometheus": {
"hashes": [
"sha256:7b44f45b18f5cc4322b206887646c1848aab42a842218875c5400333fa5d17ff",
"sha256:7b7a2a09bde96ca8e66bcf9de040a239d28f52e55f51884da9380e2d4b1c7550"
"sha256:441bd85531ecdeddacbe73c930f16de926c426869ce388fa1e8c8092f7ee5a1b",
"sha256:6e824cd407b56c01810c69d2e296940d00afe609b58818794525f9760a9a5364"
],
"index": "pypi",
"version": "==2.1.0.dev40"
"version": "==2.1.0.dev52"
},
"django-recaptcha": {
"hashes": [
@ -306,6 +306,39 @@
],
"version": "==1.0.0"
},
"elastic-apm": {
"hashes": [
"sha256:0c766621a4d15ed4ff7dd195499df1af6d7eb8c13790a727bf05773de2952de0",
"sha256:2187a0fd080cac7ed65dabfd64d7693ff187ae9b5ad4a810772387dca6877160",
"sha256:2a0bb663d3f9388db233784356f218807b9cfe1f4d4fa4569f41b567c068b50f",
"sha256:317e2a897b2a81d79bce42688975cfe0ccf6a3dc8025540c47093ea8ac5f1771",
"sha256:3a91d2df89af564dbf0abccb3d370940083205247903fe6d708fa771b16fca38",
"sha256:44fe2ce3ea57f97fce5fb32e747f6a9c9b361f5055608d59747c39ae06d1c526",
"sha256:4ca9f42d4b841ce598819f2f3a4d516c549cd5c02ab43c8283ca406c3b92a2db",
"sha256:56b34b30420aebf9566eeee3ffd633131ce51d1e2a4da6061f143a2b547d1980",
"sha256:5a56d20734771a4f7823ec12492fcd17a15dac761ecf1452d034a9b9b8b83388",
"sha256:6279cc28bd2f2bc2da478cebd5ace711b52549f736d138f950ebe0fa8f706a6f",
"sha256:69bcac2cee8f16a093f57000128caab7d1d3d8ac1474e24ce45190264ffc5ebe",
"sha256:7021b931210140e02540f3e56fdc8be07542eed10de82c9e5464dbe449a4c9aa",
"sha256:70237e1242ae461500ed455f47a5518abdbdc565e47265eddf3ca1dad530a541",
"sha256:7545f27703151ce71d73271a95662735cffb537189c214f778195a6fdab58533",
"sha256:8525ba800fbd955b65af667c43889df2358c22b1ef66ee92a846f5f4bc8d7286",
"sha256:8ba4239862f0b043d191a19e021637a25c3490f677cb8b1dd752bc425bb382e0",
"sha256:8c98625cb825c404954763ca5a6f82e06b833a6e6a9e2035065dc9894b4dc6dc",
"sha256:b02394f4d55af4f39086aee7bacf8652fde703f7226c5a564cdae9f7e2bf3f71",
"sha256:b3b1815765638ce01f9dbd136822d79e887d8d09cd10bc8770d4cc1d530bb853",
"sha256:b7bce10060abd98198d8a96e7f3e2e0e169dbd860c76e2c09e6a8874384eebb7",
"sha256:b8f849202dffe97512843dd366c4104d07d3b319e42916e3e031cff3db7475db",
"sha256:bc677614c198486ca4ef1026bde0c4efd74b936598ff9d64ea109f978a6381bb",
"sha256:d19fe00915c60ceabee42ae8c0aa76c6a48c2ffa67c5ba7f0d0fbb856ac36c09",
"sha256:d5561eb57eaa43c721258797dfab67b13938fdc94b7daec7a6ccb56dc524fe02",
"sha256:dc04aa32c7a3a17c688e3cc4c6293f2176be2482d67efccc651ff1fbb5c00ed6",
"sha256:e0d2c3463061b0e50ca53530bd5317498517d208618d90cf6e9933e93f9c727e",
"sha256:e9a416418cb2f6deb7a18b68bd75dad0552b4fd85d3e72e59ae4add0e8739b1c"
],
"index": "pypi",
"version": "==5.8.1"
},
"facebook-sdk": {
"hashes": [
"sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e",
@ -322,10 +355,10 @@
},
"idna": {
"hashes": [
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.9"
"version": "==2.10"
},
"inflection": {
"hashes": [
@ -380,36 +413,40 @@
},
"lxml": {
"hashes": [
"sha256:06748c7192eab0f48e3d35a7adae609a329c6257495d5e53878003660dc0fec6",
"sha256:0790ddca3f825dd914978c94c2545dbea5f56f008b050e835403714babe62a5f",
"sha256:1aa7a6197c1cdd65d974f3e4953764eee3d9c7b67e3966616b41fab7f8f516b7",
"sha256:22c6d34fdb0e65d5f782a4d1a1edb52e0a8365858dafb1c08cb1d16546cf0786",
"sha256:2754d4406438c83144f9ffd3628bbe2dcc6d62b20dbc5c1ec4bc4385e5d44b42",
"sha256:27ee0faf8077c7c1a589573b1450743011117f1aa1a91d5ae776bbc5ca6070f2",
"sha256:2b02c106709466a93ed424454ce4c970791c486d5fcdf52b0d822a7e29789626",
"sha256:2d1ddce96cf15f1254a68dba6935e6e0f1fe39247de631c115e84dd404a6f031",
"sha256:4f282737d187ae723b2633856085c31ae5d4d432968b7f3f478a48a54835f5c4",
"sha256:51bb4edeb36d24ec97eb3e6a6007be128b720114f9a875d6b370317d62ac80b9",
"sha256:7eee37c1b9815e6505847aa5e68f192e8a1b730c5c7ead39ff317fde9ce29448",
"sha256:7fd88cb91a470b383aafad554c3fe1ccf6dfb2456ff0e84b95335d582a799804",
"sha256:9144ce36ca0824b29ebc2e02ca186e54040ebb224292072250467190fb613b96",
"sha256:925baf6ff1ef2c45169f548cc85204433e061360bfa7d01e1be7ae38bef73194",
"sha256:a636346c6c0e1092ffc202d97ec1843a75937d8c98aaf6771348ad6422e44bb0",
"sha256:a87dbee7ad9dce3aaefada2081843caf08a44a8f52e03e0a4cc5819f8398f2f4",
"sha256:a9e3b8011388e7e373565daa5e92f6c9cb844790dc18e43073212bb3e76f7007",
"sha256:afb53edf1046599991fb4a7d03e601ab5f5422a5435c47ee6ba91ec3b61416a6",
"sha256:b26719890c79a1dae7d53acac5f089d66fd8cc68a81f4e4bd355e45470dc25e1",
"sha256:b7462cdab6fffcda853338e1741ce99706cdf880d921b5a769202ea7b94e8528",
"sha256:b77975465234ff49fdad871c08aa747aae06f5e5be62866595057c43f8d2f62c",
"sha256:c47a8a5d00060122ca5908909478abce7bbf62d812e3fc35c6c802df8fb01fe7",
"sha256:c79e5debbe092e3c93ca4aee44c9a7631bdd407b2871cb541b979fd350bbbc29",
"sha256:d8d40e0121ca1606aa9e78c28a3a7d88a05c06b3ca61630242cded87d8ce55fa",
"sha256:ee2be8b8f72a2772e72ab926a3bccebf47bb727bda41ae070dc91d1fb759b726",
"sha256:f95d28193c3863132b1f55c1056036bf580b5a488d908f7d22a04ace8935a3a9",
"sha256:fadd2a63a2bfd7fb604508e553d1cf68eca250b2fbdbd81213b5f6f2fbf23529"
"sha256:05a444b207901a68a6526948c7cc8f9fe6d6f24c70781488e32fd74ff5996e3f",
"sha256:08fc93257dcfe9542c0a6883a25ba4971d78297f63d7a5a26ffa34861ca78730",
"sha256:107781b213cf7201ec3806555657ccda67b1fccc4261fb889ef7fc56976db81f",
"sha256:121b665b04083a1e85ff1f5243d4a93aa1aaba281bc12ea334d5a187278ceaf1",
"sha256:1fa21263c3aba2b76fd7c45713d4428dbcc7644d73dcf0650e9d344e433741b3",
"sha256:2b30aa2bcff8e958cd85d907d5109820b01ac511eae5b460803430a7404e34d7",
"sha256:4b4a111bcf4b9c948e020fd207f915c24a6de3f1adc7682a2d92660eb4e84f1a",
"sha256:5591c4164755778e29e69b86e425880f852464a21c7bb53c7ea453bbe2633bbe",
"sha256:59daa84aef650b11bccd18f99f64bfe44b9f14a08a28259959d33676554065a1",
"sha256:5a9c8d11aa2c8f8b6043d845927a51eb9102eb558e3f936df494e96393f5fd3e",
"sha256:5dd20538a60c4cc9a077d3b715bb42307239fcd25ef1ca7286775f95e9e9a46d",
"sha256:74f48ec98430e06c1fa8949b49ebdd8d27ceb9df8d3d1c92e1fdc2773f003f20",
"sha256:786aad2aa20de3dbff21aab86b2fb6a7be68064cbbc0219bde414d3a30aa47ae",
"sha256:7ad7906e098ccd30d8f7068030a0b16668ab8aa5cda6fcd5146d8d20cbaa71b5",
"sha256:80a38b188d20c0524fe8959c8ce770a8fdf0e617c6912d23fc97c68301bb9aba",
"sha256:8f0ec6b9b3832e0bd1d57af41f9238ea7709bbd7271f639024f2fc9d3bb01293",
"sha256:92282c83547a9add85ad658143c76a64a8d339028926d7dc1998ca029c88ea6a",
"sha256:94150231f1e90c9595ccc80d7d2006c61f90a5995db82bccbca7944fd457f0f6",
"sha256:9dc9006dcc47e00a8a6a029eb035c8f696ad38e40a27d073a003d7d1443f5d88",
"sha256:a76979f728dd845655026ab991df25d26379a1a8fc1e9e68e25c7eda43004bed",
"sha256:aa8eba3db3d8761db161003e2d0586608092e217151d7458206e243be5a43843",
"sha256:bea760a63ce9bba566c23f726d72b3c0250e2fa2569909e2d83cda1534c79443",
"sha256:c3f511a3c58676147c277eff0224c061dd5a6a8e1373572ac817ac6324f1b1e0",
"sha256:c9d317efde4bafbc1561509bfa8a23c5cab66c44d49ab5b63ff690f5159b2304",
"sha256:cc411ad324a4486b142c41d9b2b6a722c534096963688d879ea6fa8a35028258",
"sha256:cdc13a1682b2a6241080745b1953719e7fe0850b40a5c71ca574f090a1391df6",
"sha256:cfd7c5dd3c35c19cec59c63df9571c67c6d6e5c92e0fe63517920e97f61106d1",
"sha256:e1cacf4796b20865789083252186ce9dc6cc59eca0c2e79cca332bdff24ac481",
"sha256:e70d4e467e243455492f5de463b72151cc400710ac03a0678206a5f27e79ddef",
"sha256:ecc930ae559ea8a43377e8b60ca6f8d61ac532fc57efb915d899de4a67928efd",
"sha256:f161af26f596131b63b236372e4ce40f3167c1b5b5d459b29d2514bd8c9dc9ee"
],
"index": "pypi",
"version": "==4.5.1"
"version": "==4.5.2"
},
"markupsafe": {
"hashes": [
@ -522,6 +559,7 @@
"hashes": [
"sha256:02e51e1d5828d58f154896ddfd003e2e7584869c275e5acbe290443575370fba",
"sha256:03d5cca8618620f45fd40f827423f82b86b3a202c8d44108601b0f5f56b04299",
"sha256:0e24171cf01021bc5dc17d6a9d4f33a048f09d62cc3f62541e95ef104588bda4",
"sha256:132a56abba24e2e06a479d8e5db7a48271a73a215f605017bbd476d31f8e71c1",
"sha256:1e655746f539421d923fd48df8f6f40b3443d80b75532501c0085b64afed9df5",
"sha256:2b998dc45ef5f4e5cf5248a6edfcd8d8e9fb5e35df8e4259b13a1b10eda7b16b",
@ -563,6 +601,7 @@
"sha256:2710fc8d83b3352b370db932b3710033b9d630b970ff5aaa3e7458b5336e3b32",
"sha256:35b9c9177a9fe7288b19dd41554c9c8ca1063deb426dd5a02e7e2a7416b6bd11",
"sha256:3caa32cf807422adf33c10c88c22e9e2e08b9d9d042f12e1e25fe23113dd618f",
"sha256:48cc2cfc251f04a6142badeb666d1ff49ca6fdfc303fd72579f62b768aaa52b9",
"sha256:4ae6379350a09339109e9b6f419bb2c3f03d3e441f4b0f5b8ca699d47cc9ff7e",
"sha256:4e0b27697fa1621c6d3d3b4edeec723c2e841285de6a8d378c1962da77b349be",
"sha256:58e19560814dabf5d788b95a13f6b98279cf41a49b1e49ee6cf6c79a57adb4c9",
@ -602,10 +641,10 @@
},
"pyparsing": {
"hashes": [
"sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492",
"sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a"
"sha256:1060635ca5ac864c2b7bc7b05a448df4e32d7d8c65e33cbe1514810d339672a2",
"sha256:56a551039101858c9e189ac9e66e330a03fb7079e97ba6b50193643905f450ce"
],
"version": "==3.0.0a1"
"version": "==3.0.0a2"
},
"pyrsistent": {
"hashes": [
@ -733,11 +772,11 @@
},
"sentry-sdk": {
"hashes": [
"sha256:06825c15a78934e78941ea25910db71314c891608a46492fc32c15902c6b2119",
"sha256:3ac0c430761b3cb7682ce612151d829f8644bb3830d4e530c75b02ceb745ff49"
"sha256:2de15b13836fa3522815a933bd9c887c77f4868071043349f94f1b896c1bcfb8",
"sha256:38bb09d0277117f76507c8728d9a5156f09a47ac5175bb8072513859d19a593b"
],
"index": "pypi",
"version": "==0.15.1"
"version": "==0.16.2"
},
"service-identity": {
"hashes": [
@ -779,11 +818,11 @@
},
"swagger-spec-validator": {
"hashes": [
"sha256:b651f881d718b0e3e867f19151bb47f7a50da611f285262f4d4aea092998347c",
"sha256:cb8a140c9c5d7d061d465416f156f432a92aa1a812b9c04f44e66c1568f13811"
"sha256:d1514ec7e3c058c701f27cc74f85ceb876d6418c9db57786b9c54085ed5e29eb",
"sha256:f4f23ee4dbd52bfcde90b1144dde22304add6260e9f29252e9fd7814c9b8fd16"
],
"index": "pypi",
"version": "==2.7.2"
"version": "==2.7.3"
},
"uritemplate": {
"hashes": [
@ -797,12 +836,12 @@
"secure"
],
"hashes": [
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
],
"index": "pypi",
"markers": null,
"version": "==1.25.9"
"version": "==1.25.10"
},
"vine": {
"hashes": [
@ -881,43 +920,43 @@
},
"certifi": {
"hashes": [
"sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1",
"sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
],
"version": "==2020.4.5.2"
"version": "==2020.6.20"
},
"cffi": {
"hashes": [
"sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
"sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
"sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
"sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
"sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
"sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
"sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
"sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
"sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
"sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
"sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
"sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
"sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
"sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
"sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
"sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
"sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
"sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
"sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
"sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
"sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
"sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
"sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
"sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
"sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
"sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
"sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
"sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
"sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc",
"sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9",
"sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792",
"sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2",
"sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022",
"sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8",
"sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96",
"sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2",
"sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995",
"sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1",
"sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849",
"sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c",
"sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe",
"sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3",
"sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90",
"sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f",
"sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1",
"sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf",
"sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa",
"sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc",
"sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939",
"sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e",
"sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0",
"sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9",
"sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168",
"sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33",
"sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f",
"sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"
],
"version": "==1.14.0"
"version": "==1.14.1"
},
"chardet": {
"hashes": [
@ -943,40 +982,43 @@
},
"coverage": {
"hashes": [
"sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
"sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
"sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
"sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
"sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
"sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
"sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
"sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
"sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
"sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
"sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
"sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
"sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
"sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
"sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
"sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
"sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
"sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
"sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
"sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
"sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
"sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
"sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
"sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
"sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
"sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
"sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
"sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
"sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
"sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
"sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
"sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb",
"sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3",
"sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716",
"sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034",
"sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3",
"sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8",
"sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0",
"sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f",
"sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4",
"sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962",
"sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d",
"sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b",
"sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4",
"sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3",
"sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258",
"sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59",
"sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01",
"sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd",
"sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b",
"sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d",
"sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89",
"sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd",
"sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b",
"sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d",
"sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46",
"sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546",
"sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082",
"sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b",
"sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4",
"sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8",
"sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811",
"sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd",
"sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651",
"sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"
],
"index": "pypi",
"version": "==5.1"
"version": "==5.2.1"
},
"cryptography": {
"hashes": [
@ -1004,11 +1046,11 @@
},
"django": {
"hashes": [
"sha256:5052b34b34b3425233c682e0e11d658fd6efd587d11335a0203d827224ada8f2",
"sha256:e1630333248c9b3d4e38f02093a26f1e07b271ca896d73097457996e0fae12e8"
"sha256:31a5fbbea5fc71c99e288ec0b2f00302a0a92c44b13ede80b73a6a4d6d205582",
"sha256:5457fc953ec560c5521b41fad9e6734a4668b7ba205832191bbdff40ec61073c"
],
"index": "pypi",
"version": "==3.0.7"
"version": "==3.0.8"
},
"django-debug-toolbar": {
"hashes": [
@ -1020,11 +1062,11 @@
},
"docker": {
"hashes": [
"sha256:380a20d38fbfaa872e96ee4d0d23ad9beb0f9ed57ff1c30653cbeb0c9c0964f2",
"sha256:672f51aead26d90d1cfce84a87e6f71fca401bbc2a6287be18603583620a28ba"
"sha256:03a46400c4080cb6f7aa997f881ddd84fef855499ece219d75fbdb53289c17ab",
"sha256:26eebadce7e298f55b76a88c4f8802476c5eaddbdbe38dbc6cce8781c47c9b54"
],
"index": "pypi",
"version": "==4.2.1"
"version": "==4.2.2"
},
"gitdb": {
"hashes": [
@ -1035,17 +1077,17 @@
},
"gitpython": {
"hashes": [
"sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a",
"sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"
"sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858",
"sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"
],
"version": "==3.1.3"
"version": "==3.1.7"
},
"idna": {
"hashes": [
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.9"
"version": "==2.10"
},
"isort": {
"hashes": [
@ -1125,11 +1167,11 @@
},
"pylint-django": {
"hashes": [
"sha256:06a64331c498a3f049ba669dc0c174b92209e164198d43e589b1096ee616d5f8",
"sha256:3d3436ba8d0fae576ae2db160e33a8f2746a101fda4463f2b3ff3a8b6fccec38"
"sha256:20e4d5f3987e96d29ce51ef24f13187f0d23f37a0558b6eed9b5571487ba3f4c",
"sha256:d47f278f2ef9244decc006a7412d0ea6bebe1594e6b5402703febbac036ba401"
],
"index": "pypi",
"version": "==2.0.15"
"version": "==2.2.0"
},
"pylint-plugin-utils": {
"hashes": [
@ -1171,29 +1213,29 @@
},
"regex": {
"hashes": [
"sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a",
"sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938",
"sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29",
"sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae",
"sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387",
"sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a",
"sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf",
"sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610",
"sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9",
"sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5",
"sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3",
"sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89",
"sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded",
"sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754",
"sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f",
"sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868",
"sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd",
"sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910",
"sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3",
"sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac",
"sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"
"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"
],
"version": "==2020.6.8"
"version": "==2020.7.14"
},
"requests": {
"hashes": [
@ -1233,10 +1275,10 @@
},
"stevedore": {
"hashes": [
"sha256:609912b87df5ad338ff8e44d13eaad4f4170a65b79ae9cb0aa5632598994a1b7",
"sha256:c4724f8d7b8f6be42130663855d01a9c2414d6046055b5a65ab58a0e38637688"
"sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5",
"sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"
],
"version": "==2.0.1"
"version": "==3.2.0"
},
"toml": {
"hashes": [
@ -1284,12 +1326,12 @@
"secure"
],
"hashes": [
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
],
"index": "pypi",
"markers": null,
"version": "==1.25.9"
"version": "==1.25.10"
},
"websocket-client": {
"hashes": [

View File

@ -1,11 +1,12 @@
<img src="passbook/static/static/passbook/logo.svg" height="50" alt="passbook logo"><img src="passbook/static/static/passbook/brand_inverted.svg" height="50" alt="passbook">
![CI Build status](https://img.shields.io/github/workflow/status/beryju/passbook/passbook-ci?style=flat-square)
[![CI Build status](https://img.shields.io/azure-devops/build/beryjuorg/passbook/1?style=flat-square)](https://dev.azure.com/beryjuorg/passbook/_build?definitionId=1)
![Tests](https://img.shields.io/azure-devops/tests/beryjuorg/passbook/1?compact_message&style=flat-square)
[![Code Coverage](https://img.shields.io/codecov/c/gh/beryju/passbook?style=flat-square)](https://codecov.io/gh/BeryJu/passbook)
![Docker pulls](https://img.shields.io/docker/pulls/beryju/passbook.svg?style=flat-square)
![Docker pulls (gatekeeper)](https://img.shields.io/docker/pulls/beryju/passbook-gatekeeper.svg?style=flat-square)
![Latest version](https://img.shields.io/docker/v/beryju/passbook?sort=semver&style=flat-square)
![LGTM Grade](https://img.shields.io/lgtm/grade/python/github/BeryJu/passbook?style=flat-square)
![Code Coverage](https://img.shields.io/codecov/c/gh/beryju/passbook?style=flat-square)
## What is passbook?
@ -20,7 +21,7 @@ wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
# Optionally enable Error-reporting
# export PASSBOOK_ERROR_REPORTING=true
# Optionally deploy a different version
# export PASSBOOK_TAG=0.8.15-beta
# export PASSBOOK_TAG=0.9.0-stable
# If this is a productive installation, set a different PostgreSQL Password
# export PG_PASS=$(pwgen 40 1)
docker-compose pull
@ -50,31 +51,7 @@ pipenv sync -d
```
Since passbook uses PostgreSQL-specific fields, you also need a local PostgreSQL instance to develop. passbook also uses redis for caching and message queueing.
For these databases you can use [Postgres.app](https://postgresapp.com/) and [Redis.app](https://jpadilla.github.io/redisapp/) on macOS or use it via docker-comppose:
```yaml
version: '3.7'
services:
postgresql:
container_name: postgres
image: postgres:11
volumes:
- db-data:/var/lib/postgresql/data
ports:
- 127.0.0.1:5432:5432
restart: always
redis:
container_name: redis
image: redis
ports:
- 127.0.0.1:6379:6379
restart: always
volumes:
db-data:
driver: local
```
For these databases you can use [Postgres.app](https://postgresapp.com/) and [Redis.app](https://jpadilla.github.io/redisapp/) on macOS or use it the docker-compose file in `scripts/docker-compose.yml`.
To tell passbook about these databases, create a file in the project root called `local.env.yml` with the following contents:

299
azure-pipelines.yml Normal file
View File

@ -0,0 +1,299 @@
trigger:
- master
resources:
- repo: self
variables:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
stages:
- stage: Lint
jobs:
- job: pylint
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: CmdLine@2
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run pylint passbook
- job: black
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: CmdLine@2
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run black --check passbook
- job: prospector
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: CmdLine@2
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
pipenv install --dev prospector --skip-lock
- task: CmdLine@2
inputs:
script: pipenv run prospector passbook
- job: bandit
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: CmdLine@2
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run bandit -r passbook
- job: pyright
pool:
vmImage: ubuntu-latest
steps:
- task: UseNode@1
inputs:
version: '12.x'
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: CmdLine@2
inputs:
script: npm install -g pyright
- task: CmdLine@2
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run pyright
- stage: Test
jobs:
- job: migrations
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
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run ./manage.py migrate
- job: coverage_unittest
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
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
displayName: Run full test suite
inputs:
script: |
pipenv run coverage run ./manage.py test passbook
mkdir output-unittest
mv unittest.xml output-unittest/unittest.xml
mv .coverage output-unittest/coverage
- task: PublishPipelineArtifact@1
inputs:
targetPath: 'output-unittest/'
artifact: 'coverage-unittest'
publishLocation: 'pipeline'
- job: coverage_e2e
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
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
- task: DockerCompose@0
displayName: Run ChromeDriver
inputs:
dockerComposeFile: 'e2e/ci.docker-compose.yml'
action: 'Run a specific service'
serviceName: 'chrome'
- task: CmdLine@2
displayName: Build static files for e2e
inputs:
script: |
cd passbook/static/static
yarn
- task: CmdLine@2
displayName: Run full test suite
inputs:
script: pipenv run coverage run ./manage.py test e2e
- task: CmdLine@2
displayName: Prepare unittests and coverage for upload
inputs:
script: |
mkdir output-e2e
mv unittest.xml output-e2e/unittest.xml
mv .coverage output-e2e/coverage
- task: PublishPipelineArtifact@1
condition: failed()
displayName: Upload screenshots if selenium tests fail
inputs:
targetPath: 'selenium_screenshots/'
artifact: 'selenium screenshots'
publishLocation: 'pipeline'
- task: PublishPipelineArtifact@1
inputs:
targetPath: 'output-e2e/'
artifact: 'coverage-e2e'
publishLocation: 'pipeline'
- stage: test_combine
jobs:
- job: test_coverage_combine
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'coverage-e2e'
path: "coverage-e2e/"
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'coverage-unittest'
path: "coverage-unittest/"
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: CmdLine@2
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
find .
pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage
pipenv run coverage xml
pipenv run coverage html
find .
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: 'coverage.xml'
pathToSources: '$(System.DefaultWorkingDirectory)'
- task: PublishTestResults@2
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: |
coverage-e2e/unittest.xml
coverage-unittest/unittest.xml
mergeTestResults: true
- task: CmdLine@2
env:
CODECOV_TOKEN: $(CODECOV_TOKEN)
inputs:
script: bash <(curl -s https://codecov.io/bash)
- stage: Build
jobs:
- job: build_server
pool:
vmImage: 'ubuntu-latest'
steps:
- task: Docker@2
inputs:
containerRegistry: 'dockerhub'
repository: 'beryju/passbook'
command: 'buildAndPush'
Dockerfile: 'Dockerfile'
tags: 'gh-$(Build.SourceBranchName)'
- job: build_gatekeeper
pool:
vmImage: 'ubuntu-latest'
steps:
- task: Docker@2
inputs:
containerRegistry: 'dockerhub'
repository: 'beryju/passbook-gatekeeper'
command: 'buildAndPush'
Dockerfile: 'gatekeeper/Dockerfile'
buildContext: 'gatekeeper/'
tags: 'gh-$(Build.SourceBranchName)'
- job: build_static
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/ci.docker-compose.yml'
action: 'Run services'
buildImages: false
- task: Docker@2
inputs:
containerRegistry: 'dockerhub'
repository: 'beryju/passbook-static'
command: 'build'
Dockerfile: 'static.Dockerfile'
tags: 'gh-$(Build.SourceBranchName)'
arguments: "--network=beryjupassbook_default"
- task: Docker@2
inputs:
containerRegistry: 'dockerhub'
repository: 'beryju/passbook-static'
command: 'push'
tags: 'gh-$(Build.SourceBranchName)'

View File

@ -62,7 +62,7 @@ services:
networks:
- internal
labels:
- traefik.frontend.rule=PathPrefix:/static, /robots.txt
- traefik.frontend.rule=PathPrefix:/static, /robots.txt, /favicon.ico
- traefik.port=80
- traefik.docker.network=internal
traefik:

View File

@ -53,3 +53,14 @@ Example:
```python
other_user = pb_user_by(username="other_user")
```
## Comparing IP Addresses
To compare IP Addresses or check if an IP Address is within a given subnet, you can use the functions `ip_address('192.0.2.1')` and `ip_network('192.0.2.0/24')`. With these objects you can do [arithmetic operations](https://docs.python.org/3/library/ipaddress.html#operators).
You can also check if an IP Address is within a subnet by writing the following:
```python
ip_address('192.0.2.1') in ip_network('192.0.2.0/24')
# evaluates to True
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 338 KiB

View File

@ -16,7 +16,7 @@ wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
# Optionally enable Error-reporting
# export PASSBOOK_ERROR_REPORTING=true
# Optionally deploy a different version
# export PASSBOOK_TAG=0.8.15-beta
# export PASSBOOK_TAG=0.9.0-stable
# If this is a productive installation, set a different PostgreSQL Password
# export PG_PASS=$(pwgen 40 1)
docker-compose pull

View File

@ -22,6 +22,12 @@ config:
# Log level used by web and worker
# Can be either debug, info, warning, error
log_level: warning
# Optionally enable Elastic APM Support
apm:
enabled: false
server_url: ""
secret_token: ""
verify_server_cert: true
# This Helm chart ships with built-in Prometheus ServiceMonitors and Rules.
# This requires the CoreOS Prometheus Operator.

View File

@ -16,6 +16,7 @@ Create an application in passbook and note the slug, as this will be used later.
- ACS URL: `https://signin.aws.amazon.com/saml`
- Audience: `urn:amazon:webservices`
- Issuer: `passbook`
- Binding: `Post`
You can of course use a custom signing certificate, and adjust durations.

View File

@ -26,5 +26,5 @@ return False
- `request.obj`: A Django Model instance. This is only set if the policy is ran against an object.
- `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.
- `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.

View File

@ -6,13 +6,13 @@ To export data from your old instance, run this command:
- docker-compose
```
docker-compose exec server ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
docker-compose exec server ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event otp_totp.totpdevice otp_static.staticdevice otp_static.statictoken
docker cp passbook_server_1:/tmp/passbook_dump.json passbook_dump.json
```
- kubernetes
```
kubectl exec -it passbook-web-... -- ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
kubectl exec -it passbook-web-... -- ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event otp_totp.totpdevice otp_static.staticdevice otp_static.statictoken
kubectl cp passbook-web-...:/tmp/passbook_dump.json passbook_dump.json
```

View File

@ -0,0 +1,8 @@
version: '3.7'
services:
chrome:
image: selenium/standalone-chrome
volumes:
- /dev/shm:/dev/shm
network_mode: host

View File

@ -6,15 +6,3 @@ services:
volumes:
- /dev/shm:/dev/shm
network_mode: host
postgresql:
image: postgres:11
restart: always
environment:
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_DB: passbook
network_mode: host
redis:
image: redis
restart: always
network_mode: host

View File

@ -1,300 +0,0 @@
{
"id": "7d9b2407-1520-4c04-b040-68e8ada9aecc",
"version": "2.0",
"name": "passbook",
"url": "http://localhost:8000",
"tests": [{
"id": "94b39863-74ec-4b7d-98c5-2b380b6d2c55",
"name": "passbook login simple",
"commands": [{
"id": "e60e4382-4f96-44c3-ba06-5e18609c9c2b",
"comment": "",
"command": "open",
"target": "/flows/default-authentication-flow/?next=%2F",
"targets": [],
"value": ""
}, {
"id": "b2652f24-931e-45b0-b01d-2f0ac0f74db8",
"comment": "",
"command": "click",
"target": "id=id_uid_field",
"targets": [
["id=id_uid_field", "id"],
["name=uid_field", "name"],
["css=#id_uid_field", "css:finder"],
["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
["xpath=//div/input", "xpath:position"]
],
"value": ""
}, {
"id": "f1930f8a-984a-4076-a925-20937bb2f8d3",
"comment": "",
"command": "type",
"target": "id=id_uid_field",
"targets": [
["id=id_uid_field", "id"],
["name=uid_field", "name"],
["css=#id_uid_field", "css:finder"],
["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
["xpath=//div/input", "xpath:position"]
],
"value": "admin@example.tld"
}, {
"id": "0b568ee3-1bed-4821-a3bc-f6b960dbed9d",
"comment": "",
"command": "sendKeys",
"target": "id=id_uid_field",
"targets": [
["id=id_uid_field", "id"],
["name=uid_field", "name"],
["css=#id_uid_field", "css:finder"],
["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
["xpath=//div/input", "xpath:position"]
],
"value": "${KEY_ENTER}"
}, {
"id": "6d98e479-2825-484d-996a-ccf350d2761f",
"comment": "",
"command": "type",
"target": "id=id_password",
"targets": [
["id=id_password", "id"],
["name=password", "name"],
["css=#id_password", "css:finder"],
["xpath=//input[@id='id_password']", "xpath:attributes"],
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
["xpath=//div[2]/input", "xpath:position"]
],
"value": "pbadmin"
}, {
"id": "6f7abec6-ff44-4eb5-ae23-520c1c29a706",
"comment": "",
"command": "sendKeys",
"target": "id=id_password",
"targets": [
["id=id_password", "id"],
["name=password", "name"],
["css=#id_password", "css:finder"],
["xpath=//input[@id='id_password']", "xpath:attributes"],
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
["xpath=//div[2]/input", "xpath:position"]
],
"value": "${KEY_ENTER}"
}, {
"id": "04c5876f-1405-4077-a98b-e911f09113d7",
"comment": "",
"command": "assertText",
"target": "xpath=//a[contains(@href, '/-/user/')]",
"targets": [
["linkText=pbadmin", "linkText"],
["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
["xpath=//a[contains(text(),'pbadmin')]", "xpath:link"],
["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
["xpath=//div[2]/a", "xpath:position"],
["xpath=//a[contains(.,'pbadmin')]", "xpath:innerText"]
],
"value": "pbadmin"
}]
}, {
"id": "61948b3c-3012-4f97-aa52-bc8f34fec333",
"name": "passbook enroll simple",
"commands": [{
"id": "0f4884b3-4891-41bc-956d-1fa433e892e9",
"comment": "",
"command": "open",
"target": "/flows/default-authentication-flow/?next=%2F",
"targets": [],
"value": ""
}, {
"id": "84d3861f-a60c-4650-8689-535f82b39577",
"comment": "",
"command": "click",
"target": "linkText=Sign up.",
"targets": [
["linkText=Sign up.", "linkText"],
["css=.pf-c-login__main-footer-band-item > a", "css:finder"],
["xpath=//a[contains(text(),'Sign up.')]", "xpath:link"],
["xpath=//main[@id='flow-body']/footer/div/p/a", "xpath:idRelative"],
["xpath=//a[contains(@href, '/flows/default-enrollment-flow/')]", "xpath:href"],
["xpath=//a", "xpath:position"],
["xpath=//a[contains(.,'Sign up.')]", "xpath:innerText"]
],
"value": ""
}, {
"id": "a32435ca-d84a-41e7-a915-fcbbc5f88341",
"comment": "",
"command": "type",
"target": "id=id_username",
"targets": [
["id=id_username", "id"],
["name=username", "name"],
["css=#id_username", "css:finder"],
["xpath=//input[@id='id_username']", "xpath:attributes"],
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
["xpath=//div/input", "xpath:position"]
],
"value": "foo"
}, {
"id": "3b5dcf53-8297-46c5-88b7-11c2eb25f34f",
"comment": "",
"command": "type",
"target": "id=id_password",
"targets": [
["id=id_password", "id"],
["name=password", "name"],
["css=#id_password", "css:finder"],
["xpath=//input[@id='id_password']", "xpath:attributes"],
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
["xpath=//div[2]/input", "xpath:position"]
],
"value": "pbadmin"
}, {
"id": "e948d61c-dae6-4994-b56f-ff130892b342",
"comment": "",
"command": "type",
"target": "id=id_password_repeat",
"targets": [
["id=id_password_repeat", "id"],
["name=password_repeat", "name"],
["css=#id_password_repeat", "css:finder"],
["xpath=//input[@id='id_password_repeat']", "xpath:attributes"],
["xpath=//main[@id='flow-body']/div/form/div[3]/input", "xpath:idRelative"],
["xpath=//div[3]/input", "xpath:position"]
],
"value": "pbadmin"
}, {
"id": "e7527bfc-ec74-4d96-86f0-5a3a55a59025",
"comment": "",
"command": "click",
"target": "css=.pf-c-button",
"targets": [
["css=.pf-c-button", "css:finder"],
["xpath=//button[@type='submit']", "xpath:attributes"],
["xpath=//main[@id='flow-body']/div/form/div[4]/button", "xpath:idRelative"],
["xpath=//button", "xpath:position"],
["xpath=//button[contains(.,'Continue')]", "xpath:innerText"]
],
"value": ""
}, {
"id": "434b842c-a659-4ff5-aca8-06a6a3489597",
"comment": "",
"command": "type",
"target": "id=id_name",
"targets": [
["id=id_name", "id"],
["name=name", "name"],
["css=#id_name", "css:finder"],
["xpath=//input[@id='id_name']", "xpath:attributes"],
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
["xpath=//div/input", "xpath:position"]
],
"value": "some name"
}, {
"id": "cbc43a1b-2cfe-46e2-85bc-476fb32c6cb1",
"comment": "",
"command": "type",
"target": "id=id_email",
"targets": [
["id=id_email", "id"],
["name=email", "name"],
["css=#id_email", "css:finder"],
["xpath=//input[@id='id_email']", "xpath:attributes"],
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
["xpath=//div[2]/input", "xpath:position"]
],
"value": "foo@bar.baz"
}, {
"id": "e74389a0-228b-4312-9677-e9add6358de3",
"comment": "",
"command": "click",
"target": "css=.pf-c-button",
"targets": [
["css=.pf-c-button", "css:finder"],
["xpath=//button[@type='submit']", "xpath:attributes"],
["xpath=//main[@id='flow-body']/div/form/div[3]/button", "xpath:idRelative"],
["xpath=//button", "xpath:position"],
["xpath=//button[contains(.,'Continue')]", "xpath:innerText"]
],
"value": ""
}, {
"id": "3e22f9c2-5ebd-49c2-81b1-340fa0435bbc",
"comment": "",
"command": "click",
"target": "linkText=foo",
"targets": [
["linkText=foo", "linkText"],
["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
["xpath=//a[contains(text(),'foo')]", "xpath:link"],
["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
["xpath=//div[2]/a", "xpath:position"],
["xpath=//a[contains(.,'foo')]", "xpath:innerText"]
],
"value": ""
}, {
"id": "60124cfd-f11c-4d7f-8b01-bef54c8cbd73",
"comment": "",
"command": "assertText",
"target": "xpath=//a[contains(@href, '/-/user/')]",
"targets": [
["linkText=foo", "linkText"],
["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
["xpath=//a[contains(text(),'foo')]", "xpath:link"],
["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
["xpath=//div[2]/a", "xpath:position"],
["xpath=//a[contains(.,'foo')]", "xpath:innerText"]
],
"value": "foo"
}, {
"id": "429ee61b-9991-4919-8131-55f8e1bd9a0d",
"comment": "",
"command": "assertValue",
"target": "id=id_username",
"targets": [],
"value": "foo"
}, {
"id": "f6c50760-52ed-4c1d-b232-30f8afe144eb",
"comment": "",
"command": "assertText",
"target": "id=id_name",
"targets": [
["id=id_name", "id"],
["name=name", "name"],
["css=#id_name", "css:finder"],
["xpath=//input[@id='id_name']", "xpath:attributes"],
["xpath=//main[@id='main-content']/section/div/div/div/div[2]/form/div[2]/div/input", "xpath:idRelative"],
["xpath=//div[2]/div/input", "xpath:position"]
],
"value": "some name"
}, {
"id": "b26905b5-89b5-4b41-abf5-a9f848f08622",
"comment": "",
"command": "assertText",
"target": "id=id_email",
"targets": [
["id=id_email", "id"],
["name=email", "name"],
["css=#id_email", "css:finder"],
["xpath=//input[@id='id_email']", "xpath:attributes"],
["xpath=//main[@id='main-content']/section/div/div/div/div[2]/form/div[3]/div/input", "xpath:idRelative"],
["xpath=//div[3]/div/input", "xpath:position"]
],
"value": "foo@bar.baz"
}]
}],
"suites": [{
"id": "495657fb-3f5e-4431-877c-4d0b248c0841",
"name": "Default Suite",
"persistSession": false,
"parallel": false,
"timeout": 300,
"tests": ["94b39863-74ec-4b7d-98c5-2b380b6d2c55"]
}],
"urls": ["http://localhost:8000/"],
"plugins": []
}

View File

@ -1,476 +0,0 @@
"""Test Enroll flow"""
from time import sleep
from django.test import override_settings
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
from docker import DockerClient, from_env
from docker.models.containers import Container
from docker.types import Healthcheck
from e2e.utils import USER, SeleniumTestCase
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.policies.expression.models import ExpressionPolicy
from passbook.policies.models import PolicyBinding
from passbook.stages.email.models import EmailStage, EmailTemplates
from passbook.stages.identification.models import IdentificationStage
from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage
from passbook.stages.user_login.models import UserLoginStage
from passbook.stages.user_write.models import UserWriteStage
class TestEnroll(SeleniumTestCase):
"""Test Enroll flow"""
def setUp(self):
super().setUp()
self.container = self.setup_client()
def setup_client(self) -> Container:
"""Setup test IdP container"""
client: DockerClient = from_env()
container = client.containers.run(
image="mailhog/mailhog",
detach=True,
network_mode="host",
auto_remove=True,
healthcheck=Healthcheck(
test=["CMD", "wget", "-s", "http://localhost:8025"],
interval=5 * 100 * 1000000,
start_period=1 * 100 * 1000000,
),
)
while True:
container.reload()
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
if status == "healthy":
return container
sleep(1)
def tearDown(self):
self.container.kill()
super().tearDown()
# pylint: disable=too-many-statements
def setup_test_enroll_2_step(self):
"""Setup all required objects"""
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.LINK_TEXT, "Administrate").click()
self.driver.find_element(By.LINK_TEXT, "Prompts").click()
# Create Password Prompt
self.driver.find_element(By.LINK_TEXT, "Create").click()
self.driver.find_element(By.ID, "id_field_key").send_keys("password")
self.driver.find_element(By.ID, "id_label").send_keys("Password")
dropdown = self.driver.find_element(By.ID, "id_type")
dropdown.find_element(By.XPATH, "//option[. = 'Password']").click()
self.driver.find_element(By.ID, "id_placeholder").send_keys("Password")
self.driver.find_element(By.ID, "id_order").send_keys("1")
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
# Create Password Repeat Prompt
self.driver.find_element(By.LINK_TEXT, "Create").click()
self.driver.find_element(By.ID, "id_field_key").send_keys("password_repeat")
self.driver.find_element(By.ID, "id_label").send_keys("Password (repeat)")
dropdown = self.driver.find_element(By.ID, "id_type")
dropdown.find_element(By.XPATH, "//option[. = 'Password']").click()
self.driver.find_element(By.ID, "id_placeholder").send_keys("Password (repeat)")
self.driver.find_element(By.ID, "id_order").send_keys("2")
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
# Create Name Prompt
self.driver.find_element(By.LINK_TEXT, "Create").click()
self.driver.find_element(By.ID, "id_field_key").send_keys("name")
self.driver.find_element(By.ID, "id_label").send_keys("Name")
dropdown = self.driver.find_element(By.ID, "id_type")
dropdown.find_element(By.XPATH, "//option[. = 'Text']").click()
self.driver.find_element(By.ID, "id_placeholder").send_keys("Name")
self.driver.find_element(By.ID, "id_order").send_keys("0")
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
# Create Email Prompt
self.driver.find_element(By.LINK_TEXT, "Create").click()
self.driver.find_element(By.ID, "id_field_key").send_keys("email")
self.driver.find_element(By.ID, "id_label").send_keys("Email")
dropdown = self.driver.find_element(By.ID, "id_type")
dropdown.find_element(By.XPATH, "//option[. = 'Email']").click()
self.driver.find_element(By.ID, "id_placeholder").send_keys("Email")
self.driver.find_element(By.ID, "id_order").send_keys("1")
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
self.driver.find_element(By.LINK_TEXT, "Stages").click()
# Create first enroll prompt stage
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click()
self.driver.find_element(
By.CSS_SELECTOR, "li:nth-child(9) > .pf-c-dropdown__menu-item > small"
).click()
self.driver.find_element(By.ID, "id_name").send_keys(
"enroll-prompt-stage-first"
)
dropdown = self.driver.find_element(By.ID, "id_fields")
dropdown.find_element(
By.XPATH, "//option[. = \"Prompt 'username' type=text\"]"
).click()
dropdown.find_element(
By.XPATH, "//option[. = \"Prompt 'password' type=password\"]"
).click()
dropdown.find_element(
By.XPATH, "//option[. = \"Prompt 'password_repeat' type=password\"]"
).click()
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
# Create second enroll prompt stage
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click()
self.driver.find_element(
By.CSS_SELECTOR, "li:nth-child(9) > .pf-c-dropdown__menu-item"
).click()
self.driver.find_element(By.ID, "id_name").send_keys(
"enroll-prompt-stage-second"
)
dropdown = self.driver.find_element(By.ID, "id_fields")
dropdown.find_element(
By.XPATH, "//option[. = \"Prompt 'name' type=text\"]"
).click()
dropdown.find_element(
By.XPATH, "//option[. = \"Prompt 'email' type=email\"]"
).click()
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
# Create user write stage
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click()
self.driver.find_element(
By.CSS_SELECTOR, "li:nth-child(13) > .pf-c-dropdown__menu-item"
).click()
self.driver.find_element(By.ID, "id_name").send_keys("enroll-user-write")
self.driver.find_element(By.ID, "id_name").send_keys(Keys.ENTER)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click()
# Create user login stage
self.driver.find_element(
By.CSS_SELECTOR, "li:nth-child(11) > .pf-c-dropdown__menu-item"
).click()
self.driver.find_element(By.ID, "id_name").send_keys("enroll-user-login")
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
self.driver.find_element(
By.CSS_SELECTOR,
".pf-c-nav__item:nth-child(7) .pf-c-nav__item:nth-child(1) > .pf-c-nav__link",
).click()
# Create password policy
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click()
self.driver.find_element(
By.CSS_SELECTOR, "li:nth-child(2) > .pf-c-dropdown__menu-item > small"
).click()
self.driver.find_element(By.ID, "id_name").send_keys(
"policy-enrollment-password-equals"
)
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, ".CodeMirror-scroll"))
)
self.driver.find_element(By.CSS_SELECTOR, ".CodeMirror-scroll").click()
self.driver.find_element(By.CSS_SELECTOR, ".CodeMirror textarea").send_keys(
"return request.context['password'] == request.context['password_repeat']"
)
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
# Create password policy binding
self.driver.find_element(
By.CSS_SELECTOR,
".pf-c-nav__item:nth-child(7) .pf-c-nav__item:nth-child(2) > .pf-c-nav__link",
).click()
self.driver.find_element(By.LINK_TEXT, "Create").click()
dropdown = self.driver.find_element(By.ID, "id_policy")
dropdown.find_element(
By.XPATH, '//option[. = "Policy policy-enrollment-password-equals"]'
).click()
self.driver.find_element(By.ID, "id_target").click()
dropdown = self.driver.find_element(By.ID, "id_target")
dropdown.find_element(
By.XPATH, '//option[. = "Prompt Stage enroll-prompt-stage-first"]'
).click()
self.driver.find_element(By.ID, "id_order").send_keys("0")
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
# Create Flow
self.driver.find_element(
By.CSS_SELECTOR,
".pf-c-nav__item:nth-child(6) .pf-c-nav__item:nth-child(1) > .pf-c-nav__link",
).click()
self.driver.find_element(By.LINK_TEXT, "Create").click()
self.driver.find_element(By.ID, "id_name").send_keys("Welcome")
self.driver.find_element(By.ID, "id_slug").clear()
self.driver.find_element(By.ID, "id_slug").send_keys("default-enrollment-flow")
dropdown = self.driver.find_element(By.ID, "id_designation")
dropdown.find_element(By.XPATH, '//option[. = "Enrollment"]').click()
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
self.driver.find_element(By.LINK_TEXT, "Stages").click()
# Edit identification stage
self.driver.find_element(
By.CSS_SELECTOR, "tr:nth-child(11) .pf-m-secondary"
).click()
self.driver.find_element(
By.CSS_SELECTOR,
".pf-c-form__group:nth-child(5) .pf-c-form__horizontal-group",
).click()
self.driver.find_element(By.ID, "id_enrollment_flow").click()
dropdown = self.driver.find_element(By.ID, "id_enrollment_flow")
dropdown.find_element(
By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]'
).click()
self.driver.find_element(By.ID, "id_user_fields_add_all_link").click()
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
self.driver.find_element(By.LINK_TEXT, "Bindings").click()
# Create Stage binding for first prompt stage
self.driver.find_element(By.LINK_TEXT, "Create").click()
self.driver.find_element(By.ID, "id_flow").click()
dropdown = self.driver.find_element(By.ID, "id_flow")
dropdown.find_element(
By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]'
).click()
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-form").click()
self.driver.find_element(By.ID, "id_stage").click()
dropdown = self.driver.find_element(By.ID, "id_stage")
dropdown.find_element(
By.XPATH, '//option[. = "Stage enroll-prompt-stage-first"]'
).click()
self.driver.find_element(By.ID, "id_order").click()
self.driver.find_element(By.ID, "id_order").send_keys("0")
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
# Create Stage binding for second prompt stage
self.driver.find_element(By.LINK_TEXT, "Create").click()
self.driver.find_element(By.ID, "id_flow").click()
dropdown = self.driver.find_element(By.ID, "id_flow")
dropdown.find_element(
By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]'
).click()
self.driver.find_element(By.ID, "id_stage").click()
dropdown = self.driver.find_element(By.ID, "id_stage")
dropdown.find_element(
By.XPATH, '//option[. = "Stage enroll-prompt-stage-second"]'
).click()
self.driver.find_element(By.ID, "id_order").click()
self.driver.find_element(By.ID, "id_order").send_keys("1")
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
# Create Stage binding for user write stage
self.driver.find_element(By.LINK_TEXT, "Create").click()
self.driver.find_element(By.ID, "id_flow").click()
dropdown = self.driver.find_element(By.ID, "id_flow")
dropdown.find_element(
By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]'
).click()
self.driver.find_element(By.ID, "id_stage").click()
dropdown = self.driver.find_element(By.ID, "id_stage")
dropdown.find_element(
By.XPATH, '//option[. = "Stage enroll-user-write"]'
).click()
self.driver.find_element(By.ID, "id_order").click()
self.driver.find_element(By.ID, "id_order").send_keys("2")
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
# Create Stage binding for user login stage
self.driver.find_element(By.LINK_TEXT, "Create").click()
dropdown = self.driver.find_element(By.ID, "id_flow")
dropdown.find_element(
By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]'
).click()
dropdown = self.driver.find_element(By.ID, "id_stage")
dropdown.find_element(
By.XPATH, '//option[. = "Stage enroll-user-login"]'
).click()
self.driver.find_element(By.ID, "id_order").send_keys("3")
self.driver.find_element(
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
).click()
self.driver.find_element(By.CSS_SELECTOR, "[aria-label=logout]").click()
def test_enroll_2_step(self):
"""Test 2-step enroll flow"""
self.driver.get(self.live_server_url)
self.setup_test_enroll_2_step()
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "[role=enroll]"))
)
self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click()
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
self.driver.find_element(By.ID, "id_username").send_keys("foo")
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
self.driver.find_element(By.ID, "id_name").send_keys("some name")
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
self.driver.find_element(By.LINK_TEXT, "foo").click()
self.assertEqual(
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
"foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
"some name",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
"foo@bar.baz",
)
@override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend")
def test_enroll_email(self):
"""Test enroll with Email verification"""
# First stage fields
username_prompt = Prompt.objects.create(
field_key="username", label="Username", order=0, type=FieldTypes.TEXT
)
password = Prompt.objects.create(
field_key="password", label="Password", order=1, type=FieldTypes.PASSWORD
)
password_repeat = Prompt.objects.create(
field_key="password_repeat",
label="Password (repeat)",
order=2,
type=FieldTypes.PASSWORD,
)
# Second stage fields
name_field = Prompt.objects.create(
field_key="name", label="Name", order=0, type=FieldTypes.TEXT
)
email = Prompt.objects.create(
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
)
# Stages
first_stage = PromptStage.objects.create(name="prompt-stage-first")
first_stage.fields.set([username_prompt, password, password_repeat])
first_stage.save()
second_stage = PromptStage.objects.create(name="prompt-stage-second")
second_stage.fields.set([name_field, email])
second_stage.save()
email_stage = EmailStage.objects.create(
name="enroll-email",
host="localhost",
port=1025,
template=EmailTemplates.ACCOUNT_CONFIRM,
)
user_write = UserWriteStage.objects.create(name="enroll-user-write")
user_login = UserLoginStage.objects.create(name="enroll-user-login")
# Password checking policy
password_policy = ExpressionPolicy.objects.create(
name="policy-enrollment-password-equals",
expression="return request.context['password'] == request.context['password_repeat']",
)
PolicyBinding.objects.create(
target=first_stage, policy=password_policy, order=0
)
flow = Flow.objects.create(
name="default-enrollment-flow",
slug="default-enrollment-flow",
designation=FlowDesignation.ENROLLMENT,
)
# Attach enrollment flow to identification stage
ident_stage: IdentificationStage = IdentificationStage.objects.first()
ident_stage.enrollment_flow = flow
ident_stage.save()
FlowStageBinding.objects.create(flow=flow, stage=first_stage, order=0)
FlowStageBinding.objects.create(flow=flow, stage=second_stage, order=1)
FlowStageBinding.objects.create(flow=flow, stage=user_write, order=2)
FlowStageBinding.objects.create(flow=flow, stage=email_stage, order=3)
FlowStageBinding.objects.create(flow=flow, stage=user_login, order=4)
self.driver.get(self.live_server_url)
self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click()
self.driver.find_element(By.ID, "id_username").send_keys("foo")
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
self.driver.find_element(By.ID, "id_name").send_keys("some name")
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
sleep(3)
# Open Mailhog
self.driver.get("http://localhost:8025")
# Click on first message
self.driver.find_element(By.CLASS_NAME, "msglist-message").click()
sleep(3)
self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane"))
self.driver.find_element(By.ID, "confirm").click()
self.driver.close()
self.driver.switch_to.window(self.driver.window_handles[0])
# We're now logged in
sleep(3)
self.wait.until(
ec.presence_of_element_located(
(By.XPATH, "//a[contains(@href, '/-/user/')]")
)
)
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
self.assertEqual(
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
"foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
"some name",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
"foo@bar.baz",
)

260
e2e/test_flows_enroll.py Normal file
View File

@ -0,0 +1,260 @@
"""Test Enroll flow"""
from time import sleep
from django.test import override_settings
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec
from docker import DockerClient, from_env
from docker.models.containers import Container
from docker.types import Healthcheck
from e2e.utils import USER, SeleniumTestCase
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.policies.expression.models import ExpressionPolicy
from passbook.policies.models import PolicyBinding
from passbook.stages.email.models import EmailStage, EmailTemplates
from passbook.stages.identification.models import IdentificationStage
from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage
from passbook.stages.user_login.models import UserLoginStage
from passbook.stages.user_write.models import UserWriteStage
class TestFlowsEnroll(SeleniumTestCase):
"""Test Enroll flow"""
def setUp(self):
self.container = self.setup_client()
super().setUp()
def setup_client(self) -> Container:
"""Setup test IdP container"""
client: DockerClient = from_env()
container = client.containers.run(
image="mailhog/mailhog",
detach=True,
network_mode="host",
auto_remove=True,
healthcheck=Healthcheck(
test=["CMD", "wget", "-s", "http://localhost:8025"],
interval=5 * 100 * 1000000,
start_period=1 * 100 * 1000000,
),
)
while True:
container.reload()
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
if status == "healthy":
return container
sleep(1)
def tearDown(self):
self.container.kill()
super().tearDown()
def test_enroll_2_step(self):
"""Test 2-step enroll flow"""
# First stage fields
username_prompt = Prompt.objects.create(
field_key="username", label="Username", order=0, type=FieldTypes.TEXT
)
password = Prompt.objects.create(
field_key="password", label="Password", order=1, type=FieldTypes.PASSWORD
)
password_repeat = Prompt.objects.create(
field_key="password_repeat",
label="Password (repeat)",
order=2,
type=FieldTypes.PASSWORD,
)
# Second stage fields
name_field = Prompt.objects.create(
field_key="name", label="Name", order=0, type=FieldTypes.TEXT
)
email = Prompt.objects.create(
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
)
# Stages
first_stage = PromptStage.objects.create(name="prompt-stage-first")
first_stage.fields.set([username_prompt, password, password_repeat])
first_stage.save()
second_stage = PromptStage.objects.create(name="prompt-stage-second")
second_stage.fields.set([name_field, email])
second_stage.save()
user_write = UserWriteStage.objects.create(name="enroll-user-write")
user_login = UserLoginStage.objects.create(name="enroll-user-login")
# Password checking policy
password_policy = ExpressionPolicy.objects.create(
name="policy-enrollment-password-equals",
expression="return request.context['password'] == request.context['password_repeat']",
)
PolicyBinding.objects.create(
target=first_stage, policy=password_policy, order=0
)
flow = Flow.objects.create(
name="default-enrollment-flow",
slug="default-enrollment-flow",
designation=FlowDesignation.ENROLLMENT,
)
# Attach enrollment flow to identification stage
ident_stage: IdentificationStage = IdentificationStage.objects.first()
ident_stage.enrollment_flow = flow
ident_stage.save()
FlowStageBinding.objects.create(target=flow, stage=first_stage, order=0)
FlowStageBinding.objects.create(target=flow, stage=second_stage, order=1)
FlowStageBinding.objects.create(target=flow, stage=user_write, order=2)
FlowStageBinding.objects.create(target=flow, stage=user_login, order=3)
self.driver.get(self.live_server_url)
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "[role=enroll]"))
)
self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click()
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
self.driver.find_element(By.ID, "id_username").send_keys("foo")
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
self.driver.find_element(By.ID, "id_name").send_keys("some name")
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
self.driver.find_element(By.LINK_TEXT, "foo").click()
self.wait_for_url(self.url("passbook_core:user-settings"))
self.assertEqual(
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
"foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
"some name",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
"foo@bar.baz",
)
@override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend")
def test_enroll_email(self):
"""Test enroll with Email verification"""
# First stage fields
username_prompt = Prompt.objects.create(
field_key="username", label="Username", order=0, type=FieldTypes.TEXT
)
password = Prompt.objects.create(
field_key="password", label="Password", order=1, type=FieldTypes.PASSWORD
)
password_repeat = Prompt.objects.create(
field_key="password_repeat",
label="Password (repeat)",
order=2,
type=FieldTypes.PASSWORD,
)
# Second stage fields
name_field = Prompt.objects.create(
field_key="name", label="Name", order=0, type=FieldTypes.TEXT
)
email = Prompt.objects.create(
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
)
# Stages
first_stage = PromptStage.objects.create(name="prompt-stage-first")
first_stage.fields.set([username_prompt, password, password_repeat])
first_stage.save()
second_stage = PromptStage.objects.create(name="prompt-stage-second")
second_stage.fields.set([name_field, email])
second_stage.save()
email_stage = EmailStage.objects.create(
name="enroll-email",
host="localhost",
port=1025,
template=EmailTemplates.ACCOUNT_CONFIRM,
)
user_write = UserWriteStage.objects.create(name="enroll-user-write")
user_login = UserLoginStage.objects.create(name="enroll-user-login")
# Password checking policy
password_policy = ExpressionPolicy.objects.create(
name="policy-enrollment-password-equals",
expression="return request.context['password'] == request.context['password_repeat']",
)
PolicyBinding.objects.create(
target=first_stage, policy=password_policy, order=0
)
flow = Flow.objects.create(
name="default-enrollment-flow",
slug="default-enrollment-flow",
designation=FlowDesignation.ENROLLMENT,
)
# Attach enrollment flow to identification stage
ident_stage: IdentificationStage = IdentificationStage.objects.first()
ident_stage.enrollment_flow = flow
ident_stage.save()
FlowStageBinding.objects.create(target=flow, stage=first_stage, order=0)
FlowStageBinding.objects.create(target=flow, stage=second_stage, order=1)
FlowStageBinding.objects.create(target=flow, stage=user_write, order=2)
FlowStageBinding.objects.create(target=flow, stage=email_stage, order=3)
FlowStageBinding.objects.create(target=flow, stage=user_login, order=4)
self.driver.get(self.live_server_url)
self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click()
self.driver.find_element(By.ID, "id_username").send_keys("foo")
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
self.driver.find_element(By.ID, "id_name").send_keys("some name")
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
sleep(3)
# Open Mailhog
self.driver.get("http://localhost:8025")
# Click on first message
self.driver.find_element(By.CLASS_NAME, "msglist-message").click()
sleep(3)
self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane"))
self.driver.find_element(By.ID, "confirm").click()
self.driver.close()
self.driver.switch_to.window(self.driver.window_handles[0])
# We're now logged in
sleep(3)
self.wait.until(
ec.presence_of_element_located(
(By.XPATH, "//a[contains(@href, '/-/user/')]")
)
)
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
self.assertEqual(
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
"foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
"some name",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
"foo@bar.baz",
)

View File

@ -5,7 +5,7 @@ from selenium.webdriver.common.keys import Keys
from e2e.utils import USER, SeleniumTestCase
class TestLogin(SeleniumTestCase):
class TestFlowsLogin(SeleniumTestCase):
"""test default login flow"""
def test_login(self):

View File

@ -0,0 +1,54 @@
"""test stage setup flows (password change)"""
import string
from random import SystemRandom
from time import sleep
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from e2e.utils import USER, SeleniumTestCase
from passbook.core.models import User
from passbook.flows.models import Flow, FlowDesignation
from passbook.stages.password.models import PasswordStage
class TestFlowsStageSetup(SeleniumTestCase):
"""test stage setup flows"""
def test_password_change(self):
"""test password change flow"""
# Ensure that password stage has change_flow set
flow = Flow.objects.get(
slug="default-password-change", designation=FlowDesignation.STAGE_SETUP,
)
stages = PasswordStage.objects.filter(name="default-authentication-password")
stage = stages.first()
stage.change_flow = flow
stage.save()
new_password = "".join(
SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8)
)
self.driver.get(
f"{self.live_server_url}/flows/default-authentication-flow/?next=%2F"
)
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.CSS_SELECTOR, ".pf-c-page__header").click()
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").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)
self.driver.find_element(By.ID, "id_password_repeat").click()
self.driver.find_element(By.ID, "id_password_repeat").send_keys(new_password)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
sleep(2)
# Because USER() is cached, we need to get the user manually here
user = User.objects.get(username=USER().username)
self.assertTrue(user.check_password(new_password))

View File

@ -11,6 +11,8 @@ from docker.types import Healthcheck
from e2e.utils import USER, SeleniumTestCase
from passbook.core.models import Application
from passbook.flows.models import Flow
from passbook.policies.expression.models import ExpressionPolicy
from passbook.policies.models import PolicyBinding
from passbook.providers.oauth.models import OAuth2Provider
@ -18,16 +20,16 @@ class TestProviderOAuth(SeleniumTestCase):
"""test OAuth Provider flow"""
def setUp(self):
super().setUp()
self.client_id = generate_client_id()
self.client_secret = generate_client_secret()
self.container = self.setup_client()
super().setUp()
def setup_client(self) -> Container:
"""Setup client grafana container which we test OAuth against"""
client: DockerClient = from_env()
container = client.containers.run(
image="grafana/grafana:latest",
image="grafana/grafana:7.1.0",
detach=True,
network_mode="host",
auto_remove=True,
@ -93,29 +95,28 @@ class TestProviderOAuth(SeleniumTestCase):
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url("http://localhost:3000/?orgId=1")
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().username,
)
self.assertEqual(
self.driver.find_element(
By.XPATH,
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
).get_attribute("value"),
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
"value"
),
USER().username,
)
self.assertEqual(
self.driver.find_element(
By.XPATH,
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
By.CSS_SELECTOR, "input[name=email]"
).get_attribute("value"),
USER().email,
)
self.assertEqual(
self.driver.find_element(
By.XPATH,
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
By.CSS_SELECTOR, "input[name=login]"
).get_attribute("value"),
USER().username,
)
@ -161,31 +162,69 @@ class TestProviderOAuth(SeleniumTestCase):
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]"
).text,
)
sleep(1)
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.assertEqual(
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
USER().username,
)
self.assertEqual(
self.driver.find_element(
By.XPATH,
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
).get_attribute("value"),
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
"value"
),
USER().username,
)
self.assertEqual(
self.driver.find_element(
By.XPATH,
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
By.CSS_SELECTOR, "input[name=email]"
).get_attribute("value"),
USER().email,
)
self.assertEqual(
self.driver.find_element(
By.XPATH,
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
By.CSS_SELECTOR, "input[name=login]"
).get_attribute("value"),
USER().username,
)
def test_denied(self):
"""test OAuth Provider flow (default authorization flow, denied)"""
sleep(1)
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-explicit-consent"
)
provider = OAuth2Provider.objects.create(
name="grafana",
client_type=OAuth2Provider.CLIENT_CONFIDENTIAL,
authorization_grant_type=OAuth2Provider.GRANT_AUTHORIZATION_CODE,
client_id=self.client_id,
client_secret=self.client_secret,
redirect_uris="http://localhost:3000/login/github",
skip_authorization=True,
authorization_flow=authorization_flow,
)
app = Application.objects.create(
name="Grafana", slug="grafana", 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--github").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(self.url("passbook_flows:denied"))
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
"Permission denied",
)

View File

@ -14,6 +14,8 @@ from docker.types import Healthcheck
from e2e.utils import USER, SeleniumTestCase, ensure_rsa_key
from passbook.core.models import Application
from passbook.flows.models import Flow
from passbook.policies.expression.models import ExpressionPolicy
from passbook.policies.models import PolicyBinding
from passbook.providers.oidc.models import OpenIDProvider
@ -21,16 +23,16 @@ class TestProviderOIDC(SeleniumTestCase):
"""test OpenID Provider flow"""
def setUp(self):
super().setUp()
self.client_id = generate_client_id()
self.client_secret = generate_client_secret()
self.container = self.setup_client()
super().setUp()
def setup_client(self) -> Container:
"""Setup client grafana container which we test OIDC against"""
client: DockerClient = from_env()
container = client.containers.run(
image="grafana/grafana:latest",
image="grafana/grafana:7.1.0",
detach=True,
network_mode="host",
auto_remove=True,
@ -151,23 +153,20 @@ class TestProviderOIDC(SeleniumTestCase):
USER().name,
)
self.assertEqual(
self.driver.find_element(
By.XPATH,
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
).get_attribute("value"),
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
"value"
),
USER().name,
)
self.assertEqual(
self.driver.find_element(
By.XPATH,
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
By.CSS_SELECTOR, "input[name=email]"
).get_attribute("value"),
USER().email,
)
self.assertEqual(
self.driver.find_element(
By.XPATH,
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
By.CSS_SELECTOR, "input[name=login]"
).get_attribute("value"),
USER().email,
)
@ -219,6 +218,7 @@ class TestProviderOIDC(SeleniumTestCase):
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.until(
@ -232,23 +232,67 @@ class TestProviderOIDC(SeleniumTestCase):
USER().name,
)
self.assertEqual(
self.driver.find_element(
By.XPATH,
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
).get_attribute("value"),
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
"value"
),
USER().name,
)
self.assertEqual(
self.driver.find_element(
By.XPATH,
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
By.CSS_SELECTOR, "input[name=email]"
).get_attribute("value"),
USER().email,
)
self.assertEqual(
self.driver.find_element(
By.XPATH,
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
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"
)
client = Client.objects.create(
name="grafana",
client_type="confidential",
client_id=self.client_id,
client_secret=self.client_secret,
_redirect_uris="http://localhost:3000/login/generic_oauth",
_scope="openid profile email",
reuse_consent=False,
require_consent=False,
)
# At least one of these objects must exist
ensure_rsa_key()
# This response_code object might exist or not, depending on the order the tests are run
rp_type, _ = ResponseType.objects.get_or_create(value="code")
client.response_types.set([rp_type])
client.save()
provider = OpenIDProvider.objects.create(
oidc_client=client, authorization_flow=authorization_flow,
)
app = Application.objects.create(
name="Grafana", slug="grafana", 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_for_url(self.url("passbook_flows:denied"))
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
"Permission denied",
)

View File

@ -11,13 +11,13 @@ 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.lib.utils.reflection import class_to_path
from passbook.policies.expression.models import ExpressionPolicy
from passbook.policies.models import PolicyBinding
from passbook.providers.saml.models import (
SAMLBindings,
SAMLPropertyMapping,
SAMLProvider,
)
from passbook.providers.saml.processors.generic import GenericProcessor
class TestProviderSAML(SeleniumTestCase):
@ -68,7 +68,6 @@ class TestProviderSAML(SeleniumTestCase):
)
provider: SAMLProvider = SAMLProvider.objects.create(
name="saml-test",
processor_path=class_to_path(GenericProcessor),
acs_url="http://localhost:9009/saml/acs",
audience="passbook-e2e",
issuer="passbook-e2e",
@ -88,6 +87,7 @@ class TestProviderSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url("http://localhost:9009/")
self.assertEqual(
self.driver.find_element(By.XPATH, "/html/body/pre").text,
f"Hello, {USER().name}!",
@ -101,7 +101,6 @@ class TestProviderSAML(SeleniumTestCase):
)
provider: SAMLProvider = SAMLProvider.objects.create(
name="saml-test",
processor_path=class_to_path(GenericProcessor),
acs_url="http://localhost:9009/saml/acs",
audience="passbook-e2e",
issuer="passbook-e2e",
@ -127,7 +126,9 @@ class TestProviderSAML(SeleniumTestCase):
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
).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}!",
@ -141,7 +142,6 @@ class TestProviderSAML(SeleniumTestCase):
)
provider: SAMLProvider = SAMLProvider.objects.create(
name="saml-test",
processor_path=class_to_path(GenericProcessor),
acs_url="http://localhost:9009/saml/acs",
audience="passbook-e2e",
issuer="passbook-e2e",
@ -166,7 +166,45 @@ class TestProviderSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url("http://localhost:9009/")
self.assertEqual(
self.driver.find_element(By.XPATH, "/html/body/pre").text,
f"Hello, {USER().name}!",
)
def test_sp_initiated_denied(self):
"""test SAML Provider flow SP-initiated flow (Policy denies access)"""
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
negative_policy = ExpressionPolicy.objects.create(
name="negative-static", expression="return False"
)
provider: SAMLProvider = SAMLProvider.objects.create(
name="saml-test",
acs_url="http://localhost:9009/saml/acs",
audience="passbook-e2e",
issuer="passbook-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=CertificateKeyPair.objects.first(),
)
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
app = Application.objects.create(
name="SAML", slug="passbook-saml", provider=provider,
)
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
self.container = self.setup_client(provider)
self.driver.get("http://localhost:9009/")
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url(self.url("passbook_flows:denied"))
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
"Permission denied",
)

View File

@ -35,13 +35,42 @@ NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG
aQ==
-----END CERTIFICATE-----"""
IDP_KEY = """-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNQIWjOA1vWHUz
SPM1FIKOE4GdH65VtWlpZ9dghH4CFYN0R7mvJj4KBq86Dxt8vJvLMV16GVh0NGCR
50QH8aMbxonDTqXSoXiMM4DDSQTKBYK7aZwftc7FG35gAfdNUdr8e7VbdaPOShuq
qotDyCQpZYzbt86ABnoaJ5okE3pUFIwxw97LcdYsGZz5Ngma/V1to7aMeEqHyl8r
DRbXZUzw/U8g7yC/g+G7+64liJ4FYqLEETLLSUePKLFgUJHXbF2HgIDjur3nxlEa
ecNQYVUTVCGBFpwkI5n1t3m32avwotpUFhMImjkRETyPKZpvl0+p7mop8mwJmKpa
CVuNSj23AgMBAAECggEABn4I/B20xxXcNzASiVZJvua9DdRHtmxTlkLznBj0x2oY
y1/Nbs3d3oFRn5uEuhBZOTcphsgwdRSHDXZsP3gUObew+d2N/zieUIj8hLDVlvJP
rU/s4U/l53Q0LiNByE9ThvL+zJLPCKJtd5uHZjB5fFm69+Q7gu8xg4xHIub+0pP5
PHanmHCDrbgNN/oqlar4FZ2MXTgekW6Amyc/koE9hIn4Baa2Ke/B/AUGY4pMRLqp
TArt+GTVeWeoFY9QACUpaHpJhGb/Piou6tlU57e42cLoki1f0+SARsBBKyXA7BB1
1fMH10KQYFA68dTYWlKzQau/K4xaqg4FKmtwF66GQQKBgQD9OpNUS7oRxMHVJaBR
TNWW+V1FXycqojekFpDijPb2X5CWV16oeWgaXp0nOHFdy9EWs3GtGpfZasaRVHsX
SHtPh4Nb8JqHdGE0/CD6t0+4Dns8Bn9cSqtdQB7R3Jn7IMXi9X/U8LDKo+A18/Jq
V8VgUngMny9YjMkQIbK8TRWkYQKBgQDPf4nxO6ju+tOHHORQty3bYDD0+OV3I0+L
0yz0uPreryBVi9nY43KakH52D7UZEwwsBjjGXD+WH8xEsmBWsGNXJu025PvzIJoz
lAEiXvMp/NmYp+tY4rDmO8RhyVocBqWHzh38m0IFOd4ByFD5nLEDrA3pDVo0aNgY
n0GwRysZFwKBgQDkCj3m6ZMUsUWEty+aR0EJhmKyODBDOnY09IVhH2S/FexVFzUN
LtfK9206hp/Awez3Ln2uT4Zzqq5K7fMzUniJdBWdVB004l8voeXpIe9OZuwfcBJ9
gFi1zypx/uFDv421BzQpBN+QfOdKbvbdQVFjnqCxbSDr80yVlGMrI5fbwQKBgG09
oRrepO7EIO8GN/GCruLK/ptKGkyhy3Q6xnVEmdb47hX7ncJA5IoZPmrblCVSUNsw
n11XHabksL8OBgg9rt8oQEThQv/aDzTOW9aDlJNragejiBTwq99aYeZ1gjo1CZq4
2jKubpCfyZC4rGDtrIfZYi1q+S2UcQhtd8DdhwQbAoGAAM4EpDA4yHB5yiek1p/o
CbqRCta/Dx6Eyo0KlNAyPuFPAshupG4NBx7mT2ASfL+2VBHoi6mHSri+BDX5ryYF
fMYvp7URYoq7w7qivRlvvEg5yoYrK13F2+Gj6xJ4jEN9m0KdM/g3mJGq0HBTIQrp
Sm75WXsflOxuTn08LbgGc4s=
-----END PRIVATE KEY-----"""
class TestSourceSAML(SeleniumTestCase):
"""test SAML Source flow"""
def setUp(self):
super().setUp()
self.container = self.setup_client()
super().setUp()
def setup_client(self) -> Container:
"""Setup test IdP container"""
@ -81,7 +110,7 @@ class TestSourceSAML(SeleniumTestCase):
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
keypair = CertificateKeyPair.objects.create(
name="test-idp-cert", certificate_data=IDP_CERT
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
)
SAMLSource.objects.create(
@ -125,3 +154,109 @@ class TestSourceSAML(SeleniumTestCase):
self.assertNotEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
)
def test_idp_post(self):
"""test SAML Source With post binding"""
sleep(1)
# Bootstrap all needed objects
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
keypair = CertificateKeyPair.objects.create(
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
)
SAMLSource.objects.create(
name="saml-idp-test",
slug="saml-idp-test",
authentication_flow=authentication_flow,
enrollment_flow=enrollment_flow,
issuer="entity-id",
sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
binding_type=SAMLBindingTypes.POST,
signing_kp=keypair,
)
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()
sleep(1)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
# Now we should be at the IDP, wait for the username field
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
self.driver.find_element(By.ID, "username").send_keys("user1")
self.driver.find_element(By.ID, "password").send_keys("user1pass")
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()
# Wait until we've loaded the user info page
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
self.assertNotEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
)
def test_idp_post_auto(self):
"""test SAML Source With post binding (auto redirect)"""
sleep(1)
# Bootstrap all needed objects
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
keypair = CertificateKeyPair.objects.create(
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
)
SAMLSource.objects.create(
name="saml-idp-test",
slug="saml-idp-test",
authentication_flow=authentication_flow,
enrollment_flow=enrollment_flow,
issuer="entity-id",
sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
binding_type=SAMLBindingTypes.POST_AUTO,
signing_kp=keypair,
)
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 username field
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
self.driver.find_element(By.ID, "username").send_keys("user1")
self.driver.find_element(By.ID, "password").send_keys("user1pass")
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()
# Wait until we've loaded the user info page
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
self.assertNotEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
)

212
e2e/test_sources_oauth.py Normal file
View File

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

View File

@ -3,7 +3,7 @@ from functools import lru_cache
from glob import glob
from importlib.util import module_from_spec, spec_from_file_location
from inspect import getmembers, isfunction
from os import makedirs
from os import environ, makedirs
from time import time
from Cryptodome.PublicKey import RSA
@ -16,6 +16,7 @@ from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from structlog import get_logger
from passbook.core.models import User
@ -42,12 +43,13 @@ class SeleniumTestCase(StaticLiveServerTestCase):
def setUp(self):
super().setUp()
makedirs("out", exist_ok=True)
makedirs("selenium_screenshots/", exist_ok=True)
self.driver = self._get_driver()
self.driver.maximize_window()
self.driver.implicitly_wait(5)
self.wait = WebDriverWait(self.driver, 60)
self.driver.implicitly_wait(30)
self.wait = WebDriverWait(self.driver, 50)
self.apply_default_data()
self.logger = get_logger()
def _get_driver(self) -> WebDriver:
return webdriver.Remote(
@ -56,10 +58,26 @@ class SeleniumTestCase(StaticLiveServerTestCase):
)
def tearDown(self):
self.driver.save_screenshot(f"out/{self.__class__.__name__}_{time()}.png")
if "TF_BUILD" in environ:
screenshot_file = (
f"selenium_screenshots/{self.__class__.__name__}_{time()}.png"
)
self.driver.save_screenshot(screenshot_file)
self.logger.warning("Saved screenshot", file=screenshot_file)
for line in self.driver.get_log("browser"):
self.logger.warning(
line["message"], source=line["source"], level=line["level"]
)
self.driver.quit()
super().tearDown()
def wait_for_url(self, desired_url):
"""Wait until URL is `desired_url`."""
self.wait.until(
lambda driver: driver.current_url == desired_url,
f"URL {self.driver.current_url} doesn't match expected URL {desired_url}",
)
def url(self, view, **kwargs) -> str:
"""reverse `view` with `**kwargs` into full URL using live_server_url"""
return self.live_server_url + reverse(view, kwargs=kwargs)

View File

@ -1,8 +1,8 @@
FROM quay.io/oauth2-proxy/oauth2-proxy
COPY templates /templates
ENV OAUTH2_PROXY_EMAIL_DOMAINS=*
ENV OAUTH2_PROXY_PROVIDER=oidc
ENV OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR=/templates
ENV OAUTH2_PROXY_HTTP_ADDRESS=:4180
# TODO: If service is access over HTTPS, this needs to be set to true (default), otherwise needs to be false
# ENV OAUTH2_PROXY_COOKIE_SECURE=true
ENV OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true

View File

@ -1,18 +0,0 @@
{{define "error.html"}}
<!DOCTYPE html>
<html lang="en" charset="utf-8">
<head>
<title>{{.Title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
</head>
<body>
<h2>{{.Title}}</h2>
<p>{{.Message}}</p>
<hr>
<p><a href="{{.ProxyPrefix}}/sign_in">Sign In</a></p>
</body>
</html>
{{end}}

View File

@ -1,119 +0,0 @@
{{define "sign_in.html"}}
<!DOCTYPE html>
<html lang="en" charset="utf-8">
<head>
<title>Sign In with passbook</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.42857143;
color: #333;
background: #f0f0f0;
}
.signin {
display: block;
margin: 20px auto;
max-width: 400px;
background: #fff;
border: 1px solid #ccc;
border-radius: 10px;
padding: 20px;
}
.center {
text-align: center;
}
.btn {
color: #fff;
background-color: #428bca;
border: 1px solid #357ebd;
-webkit-border-radius: 4;
-moz-border-radius: 4;
border-radius: 4px;
font-size: 14px;
padding: 6px 12px;
text-decoration: none;
cursor: pointer;
}
.btn:hover {
background-color: #3071a9;
border-color: #285e8e;
text-decoration: none;
}
label {
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
font-weight: 700;
}
input {
display: block;
width: 100%;
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
margin: 0;
box-sizing: border-box;
}
footer {
display: block;
font-size: 10px;
color: #aaa;
text-align: center;
margin-bottom: 10px;
}
footer a {
display: inline-block;
height: 25px;
line-height: 25px;
color: #aaa;
text-decoration: underline;
}
footer a:hover {
color: #aaa;
}
</style>
</head>
<body>
<div class="signin center">
<form method="GET" action="{{.ProxyPrefix}}/start">
<input type="hidden" name="rd" value="{{.Redirect}}">
<button type="submit" class="btn">Sign in with passbook</button><br />
</form>
</div>
<script>
if (window.location.hash) {
(function () {
var inputs = document.getElementsByName('rd');
for (var i = 0; i < inputs.length; i++) {
inputs[i].value += window.location.hash;
}
})();
}
</script>
</body>
</html>
{{end}}

View File

@ -1,6 +1,6 @@
apiVersion: v1
appVersion: "0.9.0-pre3"
appVersion: "0.9.0-stable"
description: A Helm chart for passbook.
name: passbook
version: "0.9.0-pre3"
version: "0.9.0-stable"
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png

View File

@ -21,3 +21,8 @@ data:
message_queue_db: 1
error_reporting: {{ .Values.config.error_reporting }}
log_level: "{{ .Values.config.log_level }}"
apm:
enabled: {{ .Values.config.apm.enabled }}
server_url: "{{ .Values.config.apm.server_url }}"
secret_token: "{{ .Values.config.apm.server_token }}"
verify_server_cert: {{ .Values.config.apm.verify_server_cert }}

View File

@ -41,5 +41,9 @@ spec:
backend:
serviceName: {{ $fullName }}-static
servicePort: http
- path: /favicon.ico
backend:
serviceName: {{ $fullName }}-static
servicePort: http
{{- end }}
{{- end }}

View File

@ -2,7 +2,7 @@
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
image:
tag: 0.9.0-pre3
tag: 0.9.0-stable
nameOverride: ""
@ -14,6 +14,12 @@ config:
# Log level used by web and worker
# Can be either debug, info, warning, error
log_level: warning
# Optionally enable Elastic APM Support
apm:
enabled: false
server_url: ""
secret_token: ""
verify_server_cert: true
# This Helm chart ships with built-in Prometheus ServiceMonitors and Rules.
# This requires the CoreOS Prometheus Operator.

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = "0.9.0-pre3"
__version__ = "0.9.0-stable"

View File

@ -122,15 +122,21 @@
{% trans 'Certificates' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:tokens' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:tokens' 'passbook_admin:token-delete' %}">
{% trans 'Tokens' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:users' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
class="pf-c-nav__link {% is_active 'passbook_admin:users' 'passbook_admin:user-create' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
{% trans 'Users' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:groups' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:groups' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
class="pf-c-nav__link {% is_active 'passbook_admin:groups' 'passbook_admin:group-create' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
{% trans 'Groups' %}
</a>
</li>

View File

@ -27,7 +27,7 @@
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
<th role="columnheader" scope="col">{% trans 'Identifier' %}</th>
<th role="columnheader" scope="col">{% trans 'Designation' %}</th>
<th role="columnheader" scope="col">{% trans 'Stages' %}</th>
<th role="columnheader" scope="col">{% trans 'Policies' %}</th>
@ -39,8 +39,8 @@
<tr role="row">
<th role="columnheader">
<div>
<div>{{ flow.name }}</div>
<small>{{ flow.slug }}</small>
<div>{{ flow.slug }}</div>
<small>{{ flow.name }}</small>
</div>
</th>
<td role="cell">
@ -61,6 +61,7 @@
<td>
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-update' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:flow-delete' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-execute' pk=flow.pk %}?next={{ request.get_full_path }}">{% trans 'Execute' %}</a>
</td>
</tr>
{% endfor %}

View File

@ -10,29 +10,33 @@
</section>
<section class="pf-c-page__main-section">
<div class="pf-l-gallery pf-m-gutter">
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-applications"></i> {% trans 'Applications' %}
</div>
</div>
<div class="pf-c-card__body">
<i class="pf-icon pf-icon-ok"></i> {{ application_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ application_count }}
</p>
</div>
</a>
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-middleware"></i> {% trans 'Sources' %}
</div>
</div>
<div class="pf-c-card__body">
<i class="pf-icon pf-icon-ok"></i> {{ source_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ source_count }}
</p>
</div>
</a>
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
@ -40,15 +44,19 @@
</div>
<div class="pf-c-card__body">
{% if providers_without_application.exists %}
<i class="pf-icon pf-icon-warning-triangle"></i> {{ provider_count }}
<p class="aggregate-status">
<i class="fa fa-exclamation-triangle"></i> {{ provider_count }}
</p>
<p>{% trans 'Warning: At least one Provider has no application assigned.' %}</p>
{% else %}
<i class="pf-icon pf-icon-ok"></i> {{ provider_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ provider_count }}
</p>
{% endif %}
</div>
</a>
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Stages' %}
@ -56,26 +64,32 @@
</div>
<div class="pf-c-card__body">
{% if stage_count < 1 %}
<i class="pficon-error-circle-o"></i> {{ stage_count }}
<p class="aggregate-status">
<i class="pficon-error-circle-o"></i> {{ stage_count }}
</p>
<p>{% trans 'No Stages configured. No Users will be able to login.' %}"></p>
{% else %}
<i class="pf-icon pf-icon-ok"></i> {{ stage_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ stage_count }}
</p>
{% endif %}
</div>
</a>
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-topology"></i> {% trans 'Flows' %}
</div>
</div>
<div class="pf-c-card__body">
<i class="pf-icon pf-icon-ok"></i> {{ flow_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ flow_count }}
</p>
</div>
</a>
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
@ -83,48 +97,71 @@
</div>
<div class="pf-c-card__body">
{% if policies_without_binding %}
<i class="pf-icon pf-icon-warning-triangle"></i> {{ policy_count }}
<p class="aggregate-status">
<i class="fa fa-exclamation-triangle"></i> {{ policy_count }}
</p>
<p>{% trans 'Policies without binding exist.' %}</p>
{% else %}
<i class="pf-icon pf-icon-ok"></i> {{ policy_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ policy_count }}
</p>
{% endif %}
</div>
</a>
<a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-migration"></i> {% trans 'Invitation' %}
</div>
</div>
<div class="pf-c-card__body">
<i class="pf-icon pf-icon-ok"></i> {{ invitation_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ invitation_count }}
</p>
</div>
</a>
<a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
</div>
</div>
<div class="pf-c-card__body">
<i class="pf-icon pf-icon-ok"></i> {{ user_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ user_count }}
</p>
</div>
</a>
<div class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
</div>
</div>
<div class="pf-c-card__body">
<i class="pf-icon pf-icon-ok"></i> {{ version }}
<p class="aggregate-status">
{% if version >= version_latest %}
<i class="fa fa-check-circle"></i> {{ version }}
{% else %}
<i class="fa fa-exclamation-triangle"></i> {{ version }}
{% endif %}
</p>
{% if version >= version_latest %}
{% blocktrans %}
Up-to-date!
{% endblocktrans %}
{% else %}
{% blocktrans with latest=version_latest %}
{{ latest }} is available!
{% endblocktrans %}
{% endif %}
</div>
</div>
<div class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
@ -132,15 +169,19 @@
</div>
<div class="pf-c-card__body">
{% if worker_count < 1 %}
<i class="pf-icon pf-icon-warning-triangle"></i> {{ worker_count }}
<p class="aggregate-status">
<i class="fa fa-exclamation-triangle"></i> {{ worker_count }}
</p>
<p>{% trans 'No workers connected.' %}</p>
{% else %}
<i class="pf-icon pf-icon-ok"></i> {{ worker_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ worker_count }}
</p>
{% endif %}
</div>
</div>
<a class="pf-c-card pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
<a class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
@ -148,13 +189,37 @@
</div>
<div class="pf-c-card__body">
{% if cached_policies < 1 %}
<i class="pf-icon pf-icon-warning-triangle"></i> {{ cached_policies }}
<p class="aggregate-status">
<i class="fa fa-exclamation-triangle"></i> {{ cached_policies }}
</p>
<p>{% trans 'No policies cached. Users may experience slow response times.' %}</p>
{% else %}
<i class="pf-icon pf-icon-ok"></i> {{ cached_policies }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ cached_policies }}
</p>
{% endif %}
</div>
</a>
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %}
</div>
</div>
<div class="pf-c-card__body">
{% if cached_flows < 1 %}
<p class="aggregate-status">
<span class="fa fa-exclamation-triangle"></span> {{ cached_flows }}
</p>
<p>{% trans 'No flows cached.' %}</p>
{% else %}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ cached_flows }}
</p>
{% endif %}
</div>
</div>
</section>
</div>
<div class="pf-c-backdrop" id="clearCacheModalRoot" hidden>
@ -163,7 +228,9 @@
<button data-modal-close class="pf-c-button pf-m-plain" type="button" aria-label="Close dialog">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Cache' %}?</h1>
<div class="pf-c-modal-box__header">
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Cache' %}?</h1>
</div>
<div class="pf-c-modal-box__body" id="modal-description">
<form method="post" id="clearForm">
{% csrf_token %}

View File

@ -28,29 +28,50 @@
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Enabled' %}</th>
<th role="columnheader" scope="col">{% trans 'Policy' %}</th>
<th role="columnheader" scope="col">{% trans 'Target' %}</th>
<th role="columnheader" scope="col">{% trans 'Enabled' %}</th>
<th role="columnheader" scope="col">{% trans 'Order' %}</th>
<th role="columnheader" scope="col">{% trans 'Timeout' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup" class="pf-m-expanded">
{% for binding in object_list %}
<tr role="row pf-c-table__expandable-row pf-m-expanded">
<th role="cell">
<div>{{ binding.enabled }}</div>
</th>
<th role="cell">
<div>{{ binding.policy }}</div>
</th>
<th role="cell">
<div>{{ binding.target|verbose_name }}</div>
</th>
<td>
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:policy-binding-update' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:policy-binding-delete' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
<tbody role="rowgroup">
{% for pbm in object_list %}
<tr role="role">
<td>
{{ pbm }}
<small>
{{ pbm|fieldtype }}
</small>
</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
{% for binding in pbm.bindings %}
<tr class="row pf-c-table__expandable-row pf-m-expanded">
<th role="cell">
<div>{{ binding.policy }}</div>
<small>
{{ binding.policy|fieldtype }}
</small>
</th>
<th role="cell">
<div>{{ binding.enabled }}</div>
</th>
<th role="cell">
<div>{{ binding.order }}</div>
</th>
<th role="cell">
<div>{{ binding.timeout }}</div>
</th>
<td class="pb-table-action" role="cell">
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:policy-binding-update' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:policy-binding-delete' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>

View File

@ -35,7 +35,7 @@
</tr>
</thead>
<tbody role="rowgroup">
{% regroup object_list by flow as grouped_bindings %}
{% regroup object_list by target as grouped_bindings %}
{% for flow in grouped_bindings %}
<tr role="role">
<td>
@ -56,9 +56,9 @@
</td>
<th role="columnheader">
<div>
<div>{{ binding.flow.slug }}</div>
<div>{{ binding.target.slug }}</div>
<small>
{{ binding.flow.name }}
{{ binding.target.name }}
</small>
</div>
</th>

View File

@ -0,0 +1,82 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load passbook_utils %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="fas fa-key"></i>
{% trans 'Tokens' %}
</h1>
<p>{% trans "Tokens are used throughout passbook for Email validation stages, Recovery keys and API access." %}</p>
</div>
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/pagination.html' %}
</div>
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Token' %}</th>
<th role="columnheader" scope="col">{% trans 'User' %}</th>
<th role="columnheader" scope="col">{% trans 'Expires?' %}</th>
<th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for token in object_list %}
<tr role="row">
<th role="columnheader">
<div>
<div>{{ token.pk }}</div>
</div>
</th>
<td role="cell">
<span>
{{ token.user }}
</span>
</td>
<td role="cell">
<span>
{{ token.expiring|yesno:"Yes,No" }}
</span>
</td>
<td role="cell">
<span>
{{ token.expires }}
</span>
</td>
<td>
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:token-delete' pk=token.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Tokens.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no tokens exist.' %}
</div>
</div>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View File

@ -12,7 +12,7 @@
{% endblock %}
{% block action %}
{% blocktrans with type=form|form_verbose_name|title %}
{% blocktrans with type=form|form_verbose_name %}
Update {{ type }}
{% endblocktrans %}
{% endblock %}

View File

@ -1,12 +1,15 @@
"""admin tests"""
from importlib import import_module
from typing import Callable
from django.forms import ModelForm
from django.shortcuts import reverse
from django.test import Client, TestCase
from django.urls.exceptions import NoReverseMatch
from passbook.admin.urls import urlpatterns
from passbook.core.models import User
from passbook.lib.utils.reflection import get_apps
class TestAdmin(TestCase):
@ -34,4 +37,28 @@ def generic_view_tester(view_name: str) -> Callable:
for url in urlpatterns:
method_name = url.name.replace("-", "_")
setattr(TestAdmin, f"test_{method_name}", generic_view_tester(url.name))
setattr(TestAdmin, f"test_view_{method_name}", generic_view_tester(url.name))
def generic_form_tester(form: ModelForm) -> Callable:
"""Test a form"""
def tester(self: TestAdmin):
form_inst = form()
self.assertFalse(form_inst.is_valid())
return tester
# Load the forms module from every app, so we have all forms loaded
for app in get_apps():
module = app.__module__.replace(".apps", ".forms")
try:
import_module(module)
except ImportError:
pass
for form_class in ModelForm.__subclasses__():
setattr(
TestAdmin, f"test_form_{form_class.__name__}", generic_form_tester(form_class)
)

View File

@ -3,7 +3,6 @@ from django.urls import path
from passbook.admin.views import (
applications,
audit,
certificate_key_pair,
debug,
flows,
@ -18,6 +17,7 @@ from passbook.admin.views import (
stages_bindings,
stages_invitations,
stages_prompts,
tokens,
users,
)
@ -42,6 +42,13 @@ urlpatterns = [
applications.ApplicationDeleteView.as_view(),
name="application-delete",
),
# Tokens
path("tokens/", tokens.TokenListView.as_view(), name="tokens"),
path(
"tokens/<uuid:pk>/delete/",
tokens.TokenDeleteView.as_view(),
name="token-delete",
),
# Sources
path("sources/", sources.SourceListView.as_view(), name="sources"),
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
@ -188,6 +195,11 @@ urlpatterns = [
path(
"flows/<uuid:pk>/update/", flows.FlowUpdateView.as_view(), name="flow-update",
),
path(
"flows/<uuid:pk>/execute/",
flows.FlowDebugExecuteView.as_view(),
name="flow-execute",
),
path(
"flows/<uuid:pk>/delete/", flows.FlowDeleteView.as_view(), name="flow-delete",
),
@ -252,8 +264,6 @@ urlpatterns = [
certificate_key_pair.CertificateKeyPairDeleteView.as_view(),
name="certificatekeypair-delete",
),
# Audit Log
path("audit/", audit.EventListView.as_view(), name="audit-log"),
# Groups
path("groups/", groups.GroupListView.as_view(), name="groups"),
# Debug

View File

@ -1,5 +1,4 @@
"""passbook Application administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
@ -7,9 +6,10 @@ from django.contrib.auth.mixins import (
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.core.forms.applications import ApplicationForm
from passbook.core.models import Application
from passbook.lib.views import CreateAssignPermView
@ -24,9 +24,6 @@ class ApplicationListView(LoginRequiredMixin, PermissionListMixin, ListView):
paginate_by = 40
template_name = "administration/application/list.html"
def get_queryset(self):
return super().get_queryset().select_subclasses()
class ApplicationCreateView(
SuccessMessageMixin,
@ -44,10 +41,6 @@ class ApplicationCreateView(
success_url = reverse_lazy("passbook_admin:applications")
success_message = _("Successfully created Application")
def get_context_data(self, **kwargs):
kwargs["type"] = "Application"
return super().get_context_data(**kwargs)
class ApplicationUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
@ -64,7 +57,7 @@ class ApplicationUpdateView(
class ApplicationDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete application"""
@ -74,7 +67,3 @@ class ApplicationDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:applications")
success_message = _("Successfully deleted Application")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,5 +1,4 @@
"""passbook CertificateKeyPair administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
@ -7,9 +6,10 @@ from django.contrib.auth.mixins import (
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.crypto.forms import CertificateKeyPairForm
from passbook.crypto.models import CertificateKeyPair
from passbook.lib.views import CreateAssignPermView
@ -41,10 +41,6 @@ class CertificateKeyPairCreateView(
success_url = reverse_lazy("passbook_admin:certificate_key_pair")
success_message = _("Successfully created CertificateKeyPair")
def get_context_data(self, **kwargs):
kwargs["type"] = "Certificate-Key Pair"
return super().get_context_data(**kwargs)
class CertificateKeyPairUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
@ -61,7 +57,7 @@ class CertificateKeyPairUpdateView(
class CertificateKeyPairDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete certificatekeypair"""
@ -71,7 +67,3 @@ class CertificateKeyPairDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:certificate_key_pair")
success_message = _("Successfully deleted Certificate-Key Pair")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,17 +1,21 @@
"""passbook Flow administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpRequest, HttpResponse
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from django.views.generic import DetailView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.flows.forms import FlowForm
from passbook.flows.models import Flow
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
from passbook.flows.views import SESSION_KEY_PLAN, FlowPlanner
from passbook.lib.utils.urls import redirect_with_qs
from passbook.lib.views import CreateAssignPermView
@ -41,10 +45,6 @@ class FlowCreateView(
success_url = reverse_lazy("passbook_admin:flows")
success_message = _("Successfully created Flow")
def get_context_data(self, **kwargs):
kwargs["type"] = "Flow"
return super().get_context_data(**kwargs)
class FlowUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
@ -60,9 +60,7 @@ class FlowUpdateView(
success_message = _("Successfully updated Flow")
class FlowDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
class FlowDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete flow"""
model = Flow
@ -72,6 +70,21 @@ class FlowDeleteView(
success_url = reverse_lazy("passbook_admin:flows")
success_message = _("Successfully deleted Flow")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)
class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""Debug exectue flow, setting the current user as pending user"""
model = Flow
permission_required = "passbook_flows.view_flow"
# pylint: disable=unused-argument
def get(self, request: HttpRequest, pk: str) -> HttpResponse:
"""Debug exectue flow, setting the current user as pending user"""
flow: Flow = self.get_object()
planner = FlowPlanner(flow)
planner.use_cache = False
plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user})
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"passbook_flows:flow-executor-shell", self.request.GET, flow_slug=flow.slug,
)

View File

@ -1,5 +1,4 @@
"""passbook Group administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
@ -7,9 +6,10 @@ from django.contrib.auth.mixins import (
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.core.forms.groups import GroupForm
from passbook.core.models import Group
from passbook.lib.views import CreateAssignPermView
@ -41,10 +41,6 @@ class GroupCreateView(
success_url = reverse_lazy("passbook_admin:groups")
success_message = _("Successfully created Group")
def get_context_data(self, **kwargs):
kwargs["type"] = "Group"
return super().get_context_data(**kwargs)
class GroupUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
@ -60,15 +56,12 @@ class GroupUpdateView(
success_message = _("Successfully updated Group")
class GroupDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView):
class GroupDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete group"""
model = Group
permission_required = "passbook_flows.delete_group"
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:groups")
success_message = _("Successfully deleted Group")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -2,6 +2,8 @@
from django.core.cache import cache
from django.shortcuts import redirect, reverse
from django.views.generic import TemplateView
from packaging.version import Version, parse
from requests import RequestException, get
from passbook import __version__
from passbook.admin.mixins import AdminRequiredMixin
@ -11,6 +13,22 @@ 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() -> 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"""
@ -33,7 +51,8 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
kwargs["stage_count"] = len(Stage.objects.all())
kwargs["flow_count"] = len(Flow.objects.all())
kwargs["invitation_count"] = len(Invitation.objects.all())
kwargs["version"] = __version__
kwargs["version"] = parse(__version__)
kwargs["version_latest"] = latest_version()
kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
kwargs["providers_without_application"] = Provider.objects.filter(
application=None
@ -42,4 +61,5 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
Policy.objects.filter(bindings__isnull=True)
)
kwargs["cached_policies"] = len(cache.keys("policy_*"))
kwargs["cached_flows"] = len(cache.keys("flow_*"))
return super().get_context_data(**kwargs)

View File

@ -8,22 +8,25 @@ from django.contrib.auth.mixins import (
)
from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import QuerySet
from django.forms import Form
from django.http import Http404, HttpRequest, HttpResponse
from django.http import HttpResponse
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, FormView, ListView, UpdateView
from django.views.generic import FormView
from django.views.generic.detail import DetailView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.forms.policies import PolicyTestForm
from passbook.lib.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView
from passbook.admin.views.utils import (
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
)
from passbook.policies.models import Policy, PolicyBinding
from passbook.policies.process import PolicyProcess, PolicyRequest
class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
class PolicyListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
"""Show list of all policies"""
model = Policy
@ -32,19 +35,12 @@ class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
ordering = "name"
template_name = "administration/policy/list.html"
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs["types"] = {x.__name__: x for x in all_subclasses(Policy)}
return super().get_context_data(**kwargs)
def get_queryset(self) -> QuerySet:
return super().get_queryset().select_subclasses()
class PolicyCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
InheritanceCreateView,
):
"""Create new Policy"""
@ -55,24 +51,12 @@ class PolicyCreateView(
success_url = reverse_lazy("passbook_admin:policies")
success_message = _("Successfully created Policy")
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self) -> Form:
policy_type = self.request.GET.get("type")
try:
model = next(x for x in all_subclasses(Policy) if x.__name__ == policy_type)
except StopIteration as exc:
raise Http404 from exc
return path_to_class(model.form)
class PolicyUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,
):
"""Update policy"""
@ -83,27 +67,8 @@ class PolicyUpdateView(
success_url = reverse_lazy("passbook_admin:policies")
success_message = _("Successfully updated Policy")
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self) -> Form:
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None) -> Policy:
return (
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
class PolicyDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
class PolicyDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete policy"""
model = Policy
@ -113,15 +78,6 @@ class PolicyDeleteView(
success_url = reverse_lazy("passbook_admin:policies")
success_message = _("Successfully deleted Policy")
def get_object(self, queryset=None) -> Policy:
return (
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
def delete(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)
class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, FormView):
"""View to test policy(s)"""

View File

@ -1,18 +1,19 @@
"""passbook PolicyBinding administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import QuerySet
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.lib.views import CreateAssignPermView
from passbook.policies.forms import PolicyBindingForm
from passbook.policies.models import PolicyBinding
from passbook.policies.models import PolicyBinding, PolicyBindingModel
class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
@ -22,7 +23,20 @@ class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
permission_required = "passbook_policies.view_policybinding"
paginate_by = 10
ordering = ["order", "target"]
template_name = "administration/policybinding/list.html"
template_name = "administration/policy_binding/list.html"
def get_queryset(self) -> QuerySet:
# 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)
.select_subclasses()
.select_related()
.order_by("pk")
)
for pbm in objects:
pbm.bindings = PolicyBinding.objects.filter(target__pk=pbm.pbm_uuid)
return objects
class PolicyBindingCreateView(
@ -55,16 +69,9 @@ class PolicyBindingUpdateView(
success_url = reverse_lazy("passbook_admin:policies-bindings")
success_message = _("Successfully updated PolicyBinding")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
class PolicyBindingDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete policybinding"""
@ -74,7 +81,3 @@ class PolicyBindingDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:policies-bindings")
success_message = _("Successfully deleted PolicyBinding")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,22 +1,25 @@
"""passbook PropertyMapping administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
)
from passbook.core.models import PropertyMapping
from passbook.lib.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView
class PropertyMappingListView(LoginRequiredMixin, PermissionListMixin, ListView):
class PropertyMappingListView(
LoginRequiredMixin, PermissionListMixin, InheritanceListView
):
"""Show list of all property_mappings"""
model = PropertyMapping
@ -25,19 +28,12 @@ class PropertyMappingListView(LoginRequiredMixin, PermissionListMixin, ListView)
ordering = "name"
paginate_by = 40
def get_context_data(self, **kwargs):
kwargs["types"] = {x.__name__: x for x in all_subclasses(PropertyMapping)}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses()
class PropertyMappingCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
InheritanceCreateView,
):
"""Create new PropertyMapping"""
@ -48,38 +44,12 @@ class PropertyMappingCreateView(
success_url = reverse_lazy("passbook_admin:property-mappings")
success_message = _("Successfully created Property Mapping")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
property_mapping_type = self.request.GET.get("type")
try:
model = next(
x
for x in all_subclasses(PropertyMapping)
if x.__name__ == property_mapping_type
)
except StopIteration as exc:
raise Http404 from exc
kwargs["type"] = model._meta.verbose_name
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self):
property_mapping_type = self.request.GET.get("type")
try:
model = next(
x
for x in all_subclasses(PropertyMapping)
if x.__name__ == property_mapping_type
)
except StopIteration as exc:
raise Http404 from exc
return path_to_class(model.form)
class PropertyMappingUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,
):
"""Update property_mapping"""
@ -90,28 +60,9 @@ class PropertyMappingUpdateView(
success_url = reverse_lazy("passbook_admin:property-mappings")
success_message = _("Successfully updated Property Mapping")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self):
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None):
return (
PropertyMapping.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)
class PropertyMappingDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete property_mapping"""
@ -121,14 +72,3 @@ class PropertyMappingDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:property-mappings")
success_message = _("Successfully deleted Property Mapping")
def get_object(self, queryset=None):
return (
PropertyMapping.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,22 +1,23 @@
"""passbook Provider administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
)
from passbook.core.models import Provider
from passbook.lib.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView
class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
class ProviderListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
"""Show list of all providers"""
model = Provider
@ -25,19 +26,12 @@ class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
paginate_by = 10
ordering = "id"
def get_context_data(self, **kwargs):
kwargs["types"] = {x.__name__: x for x in all_subclasses(Provider)}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses()
class ProviderCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
InheritanceCreateView,
):
"""Create new Provider"""
@ -48,19 +42,12 @@ class ProviderCreateView(
success_url = reverse_lazy("passbook_admin:providers")
success_message = _("Successfully created Provider")
def get_form_class(self):
provider_type = self.request.GET.get("type")
try:
model = next(
x for x in all_subclasses(Provider) if x.__name__ == provider_type
)
except StopIteration as exc:
raise Http404 from exc
return path_to_class(model.form)
class ProviderUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,
):
"""Update provider"""
@ -71,21 +58,9 @@ class ProviderUpdateView(
success_url = reverse_lazy("passbook_admin:providers")
success_message = _("Successfully updated Provider")
def get_form_class(self):
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None):
return (
Provider.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)
class ProviderDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete provider"""
@ -95,14 +70,3 @@ class ProviderDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:providers")
success_message = _("Successfully deleted Provider")
def get_object(self, queryset=None):
return (
Provider.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,22 +1,23 @@
"""passbook Source administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
)
from passbook.core.models import Source
from passbook.lib.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView
class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
class SourceListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
"""Show list of all sources"""
model = Source
@ -25,19 +26,12 @@ class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
paginate_by = 40
template_name = "administration/source/list.html"
def get_context_data(self, **kwargs):
kwargs["types"] = {x.__name__: x for x in all_subclasses(Source)}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses()
class SourceCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
InheritanceCreateView,
):
"""Create new Source"""
@ -48,17 +42,12 @@ class SourceCreateView(
success_url = reverse_lazy("passbook_admin:sources")
success_message = _("Successfully created Source")
def get_form_class(self):
source_type = self.request.GET.get("type")
try:
model = next(x for x in all_subclasses(Source) if x.__name__ == source_type)
except StopIteration as exc:
raise Http404 from exc
return path_to_class(model.form)
class SourceUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,
):
"""Update source"""
@ -69,20 +58,8 @@ class SourceUpdateView(
success_url = reverse_lazy("passbook_admin:sources")
success_message = _("Successfully updated Source")
def get_form_class(self):
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None):
return (
Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
class SourceDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
class SourceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete source"""
model = Source
@ -91,12 +68,3 @@ class SourceDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:sources")
success_message = _("Successfully deleted Source")
def get_object(self, queryset=None):
return (
Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,22 +1,23 @@
"""passbook Stage administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
)
from passbook.flows.models import Stage
from passbook.lib.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView
class StageListView(LoginRequiredMixin, PermissionListMixin, ListView):
class StageListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
"""Show list of all stages"""
model = Stage
@ -25,19 +26,12 @@ class StageListView(LoginRequiredMixin, PermissionListMixin, ListView):
ordering = "name"
paginate_by = 40
def get_context_data(self, **kwargs):
kwargs["types"] = {x.__name__: x for x in all_subclasses(Stage)}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses()
class StageCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
InheritanceCreateView,
):
"""Create new Stage"""
@ -48,24 +42,12 @@ class StageCreateView(
success_url = reverse_lazy("passbook_admin:stages")
success_message = _("Successfully created Stage")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
stage_type = self.request.GET.get("type")
model = next(x for x in all_subclasses(Stage) if x.__name__ == stage_type)
kwargs["type"] = model._meta.verbose_name
return kwargs
def get_form_class(self):
stage_type = self.request.GET.get("type")
try:
model = next(x for x in all_subclasses(Stage) if x.__name__ == stage_type)
except StopIteration as exc:
raise Http404 from exc
return path_to_class(model.form)
class StageUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,
):
"""Update stage"""
@ -75,20 +57,8 @@ class StageUpdateView(
success_url = reverse_lazy("passbook_admin:stages")
success_message = _("Successfully updated Stage")
def get_form_class(self):
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None):
return (
Stage.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
class StageDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
class StageDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete stage"""
model = Stage
@ -96,12 +66,3 @@ class StageDeleteView(
permission_required = "passbook_flows.delete_stage"
success_url = reverse_lazy("passbook_admin:stages")
success_message = _("Successfully deleted Stage")
def get_object(self, queryset=None):
return (
Stage.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,5 +1,4 @@
"""passbook StageBinding administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
@ -7,9 +6,10 @@ from django.contrib.auth.mixins import (
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.flows.forms import FlowStageBindingForm
from passbook.flows.models import FlowStageBinding
from passbook.lib.views import CreateAssignPermView
@ -21,7 +21,7 @@ class StageBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
model = FlowStageBinding
permission_required = "passbook_flows.view_flowstagebinding"
paginate_by = 10
ordering = ["flow", "order"]
ordering = ["target", "order"]
template_name = "administration/stage_binding/list.html"
@ -41,13 +41,6 @@ class StageBindingCreateView(
success_url = reverse_lazy("passbook_admin:stage-bindings")
success_message = _("Successfully created StageBinding")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
class StageBindingUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
@ -62,16 +55,9 @@ class StageBindingUpdateView(
success_url = reverse_lazy("passbook_admin:stage-bindings")
success_message = _("Successfully updated StageBinding")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
class StageBindingDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete FlowStageBinding"""
@ -81,7 +67,3 @@ class StageBindingDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:stage-bindings")
success_message = _("Successfully deleted FlowStageBinding")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,5 +1,4 @@
"""passbook Invitation administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
@ -8,13 +7,14 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView
from django.views.generic import ListView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.core.signals import invitation_created
from passbook.admin.views.utils import DeleteMessageView
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):
@ -43,10 +43,6 @@ class InvitationCreateView(
success_url = reverse_lazy("passbook_admin:stage-invitations")
success_message = _("Successfully created Invitation")
def get_context_data(self, **kwargs):
kwargs["type"] = "Invitation"
return super().get_context_data(**kwargs)
def form_valid(self, form):
obj = form.save(commit=False)
obj.created_by = self.request.user
@ -56,7 +52,7 @@ class InvitationCreateView(
class InvitationDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete invitation"""
@ -66,7 +62,3 @@ class InvitationDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:stage-invitations")
success_message = _("Successfully deleted Invitation")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,5 +1,4 @@
"""passbook Prompt administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
@ -7,9 +6,10 @@ from django.contrib.auth.mixins import (
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.lib.views import CreateAssignPermView
from passbook.stages.prompt.forms import PromptAdminForm
from passbook.stages.prompt.models import Prompt
@ -41,10 +41,6 @@ class PromptCreateView(
success_url = reverse_lazy("passbook_admin:stage-prompts")
success_message = _("Successfully created Prompt")
def get_context_data(self, **kwargs):
kwargs["type"] = "Prompt"
return super().get_context_data(**kwargs)
class PromptUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
@ -60,9 +56,7 @@ class PromptUpdateView(
success_message = _("Successfully updated Prompt")
class PromptDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
class PromptDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete prompt"""
model = Prompt
@ -71,7 +65,3 @@ class PromptDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:stage-prompts")
success_message = _("Successfully deleted Prompt")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -0,0 +1,30 @@
"""passbook Token administration"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import ListView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.core.models import Token
class TokenListView(LoginRequiredMixin, PermissionListMixin, 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"
class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete token"""
model = Token
permission_required = "passbook_core.delete_token"
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:tokens")
success_message = _("Successfully deleted Token")

View File

@ -5,10 +5,12 @@ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.http import urlencode
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, DetailView, ListView, UpdateView
from django.views.generic import DetailView, ListView, UpdateView
from guardian.mixins import (
PermissionListMixin,
PermissionRequiredMixin,
@ -16,6 +18,7 @@ from guardian.mixins import (
)
from passbook.admin.forms.users import UserForm
from passbook.admin.views.utils import DeleteMessageView
from passbook.core.models import Token, User
from passbook.lib.views import CreateAssignPermView
@ -66,9 +69,7 @@ class UserUpdateView(
success_message = _("Successfully updated User")
class UserDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
class UserDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete user"""
model = User
@ -80,10 +81,6 @@ class UserDeleteView(
success_url = reverse_lazy("passbook_admin:users")
success_message = _("Successfully deleted User")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)
class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""Get Password reset link for user"""
@ -91,13 +88,13 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
model = User
permission_required = "passbook_core.reset_user_password"
def get(self, request, *args, **kwargs):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Create token for user and return link"""
super().get(request, *args, **kwargs)
# TODO: create plan for user, get token
token = Token.objects.create(user=self.object)
querystring = urlencode({"token": token.token_uuid})
link = request.build_absolute_uri(
reverse("passbook_flows:default-recovery", kwargs={"token": token.uuid})
reverse("passbook_flows:default-recovery") + f"?{querystring}"
)
messages.success(
request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link})

View File

@ -0,0 +1,71 @@
"""passbook admin util views"""
from typing import Any, Dict
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.views.generic import DeleteView, ListView, UpdateView
from passbook.lib.utils.reflection import all_subclasses
from passbook.lib.views import CreateAssignPermView
class DeleteMessageView(SuccessMessageMixin, DeleteView):
"""DeleteView which shows `self.success_message` on successful deletion"""
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)
class InheritanceListView(ListView):
"""ListView for objects using InheritanceManager"""
def get_context_data(self, **kwargs):
kwargs["types"] = {x.__name__: x for x in all_subclasses(self.model)}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses()
class InheritanceCreateView(CreateAssignPermView):
"""CreateView for objects using InheritanceManager"""
def get_form_class(self):
provider_type = self.request.GET.get("type")
try:
model = next(
x for x in all_subclasses(self.model) if x.__name__ == provider_type
)
except StopIteration as exc:
raise Http404 from exc
return model.form(model)
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
class InheritanceUpdateView(UpdateView):
"""UpdateView for objects using InheritanceManager"""
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self):
return self.get_object().form()
def get_object(self, queryset=None):
return (
self.model.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)

43
passbook/api/auth.py Normal file
View File

@ -0,0 +1,43 @@
"""API Authentication"""
from base64 import b64decode
from typing import Any, Tuple, Union
from django.utils.translation import gettext as _
from rest_framework import HTTP_HEADER_ENCODING, exceptions
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework.request import Request
from passbook.core.models import Token, TokenIntents, User
class PassbookTokenAuthentication(BaseAuthentication):
"""Token-based authentication using HTTP Basic authentication"""
def authenticate(self, request: Request) -> Union[Tuple[User, Any], None]:
"""Token-based authentication using HTTP Basic authentication"""
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b"basic":
return None
if len(auth) == 1:
msg = _("Invalid basic header. No credentials provided.")
raise exceptions.AuthenticationFailed(msg)
if len(auth) > 2:
msg = _(
"Invalid basic header. Credentials string should not contain spaces."
)
raise exceptions.AuthenticationFailed(msg)
header_data = b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(":")
tokens = Token.filter_not_expired(
token_uuid=header_data[2], intent=TokenIntents.INTENT_API
)
if not tokens.exists():
raise exceptions.AuthenticationFailed(_("Invalid token."))
return (tokens.first().user, None)
def authenticate_header(self, request: Request) -> str:
return 'Basic realm="passbook"'

View File

@ -6,5 +6,5 @@ from passbook.api.v2.urls import urlpatterns as v2_urls
urlpatterns = [
path("v1/", include(v1_urls)),
path("v2/", include(v2_urls)),
path("v2beta/", include(v2_urls)),
]

View File

@ -4,7 +4,6 @@ from django.urls import path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import routers
from structlog import get_logger
from passbook.api.permissions import CustomObjectPermissions
from passbook.audit.api import EventViewSet
@ -16,11 +15,11 @@ from passbook.core.api.providers import ProviderViewSet
from passbook.core.api.sources import SourceViewSet
from passbook.core.api.users import UserViewSet
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
from passbook.lib.utils.reflection import get_apps
from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet
from passbook.policies.dummy.api import DummyPolicyViewSet
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
from passbook.policies.expression.api import ExpressionPolicyViewSet
from passbook.policies.group_membership.api import GroupMembershipPolicyViewSet
from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet
from passbook.policies.password.api import PasswordPolicyViewSet
from passbook.policies.reputation.api import ReputationPolicyViewSet
@ -32,11 +31,14 @@ from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceView
from passbook.sources.oauth.api import OAuthSourceViewSet
from passbook.sources.saml.api import SAMLSourceViewSet
from passbook.stages.captcha.api import CaptchaStageViewSet
from passbook.stages.consent.api import ConsentStageViewSet
from passbook.stages.dummy.api import DummyStageViewSet
from passbook.stages.email.api import EmailStageViewSet
from passbook.stages.identification.api import IdentificationStageViewSet
from passbook.stages.invitation.api import InvitationStageViewSet, InvitationViewSet
from passbook.stages.otp.api import OTPStageViewSet
from passbook.stages.otp_static.api import OTPStaticStageViewSet
from passbook.stages.otp_time.api import OTPTimeStageViewSet
from passbook.stages.otp_validate.api import OTPValidateStageViewSet
from passbook.stages.password.api import PasswordStageViewSet
from passbook.stages.prompt.api import PromptStageViewSet, PromptViewSet
from passbook.stages.user_delete.api import UserDeleteStageViewSet
@ -44,15 +46,8 @@ from passbook.stages.user_login.api import UserLoginStageViewSet
from passbook.stages.user_logout.api import UserLogoutStageViewSet
from passbook.stages.user_write.api import UserWriteStageViewSet
LOGGER = get_logger()
router = routers.DefaultRouter()
for _passbook_app in get_apps():
if hasattr(_passbook_app, "api_mountpoint"):
for prefix, viewset in _passbook_app.api_mountpoint:
router.register(prefix, viewset)
LOGGER.debug("Mounted API URLs", app_name=_passbook_app.name)
router.register("core/applications", ApplicationViewSet)
router.register("core/groups", GroupViewSet)
router.register("core/users", UserViewSet)
@ -68,9 +63,10 @@ router.register("sources/oauth", OAuthSourceViewSet)
router.register("policies/all", PolicyViewSet)
router.register("policies/bindings", PolicyBindingViewSet)
router.register("policies/expression", ExpressionPolicyViewSet)
router.register("policies/group_membership", GroupMembershipPolicyViewSet)
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
router.register("policies/password_expiry", PasswordExpiryPolicyViewSet)
router.register("policies/password", PasswordPolicyViewSet)
router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet)
router.register("policies/reputation", ReputationPolicyViewSet)
router.register("providers/all", ProviderViewSet)
@ -85,14 +81,17 @@ router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
router.register("stages/all", StageViewSet)
router.register("stages/captcha", CaptchaStageViewSet)
router.register("stages/consent", ConsentStageViewSet)
router.register("stages/email", EmailStageViewSet)
router.register("stages/identification", IdentificationStageViewSet)
router.register("stages/invitation", InvitationStageViewSet)
router.register("stages/invitation/invitations", InvitationViewSet)
router.register("stages/otp", OTPStageViewSet)
router.register("stages/otp_static", OTPStaticStageViewSet)
router.register("stages/otp_time", OTPTimeStageViewSet)
router.register("stages/otp_validate", OTPValidateStageViewSet)
router.register("stages/password", PasswordStageViewSet)
router.register("stages/prompt/stages", PromptStageViewSet)
router.register("stages/prompt/prompts", PromptViewSet)
router.register("stages/prompt/stages", PromptStageViewSet)
router.register("stages/user_delete", UserDeleteStageViewSet)
router.register("stages/user_login", UserLoginStageViewSet)
router.register("stages/user_logout", UserLogoutStageViewSet)

View File

@ -12,6 +12,7 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.http import HttpRequest
from django.utils.translation import gettext as _
from django.views.debug import CLEANSED_SUBSTITUTE, HIDDEN_SETTINGS
from guardian.shortcuts import get_anonymous_user
from structlog import get_logger
@ -20,6 +21,22 @@ from passbook.lib.utils.http import get_client_ip
LOGGER = get_logger()
def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
"""Cleanse a dictionary, recursively"""
final_dict = {}
for key, value in source.items():
try:
if HIDDEN_SETTINGS.search(key):
final_dict[key] = CLEANSED_SUBSTITUTE
else:
final_dict[key] = value
except TypeError:
final_dict[key] = value
if isinstance(value, dict):
final_dict[key] = cleanse_dict(value)
return final_dict
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 {
@ -27,15 +44,16 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
name: str,
pk: Any
}"""
final_dict = {}
for key, value in source.items():
if isinstance(value, dict):
source[key] = sanitize_dict(value)
final_dict[key] = sanitize_dict(value)
elif isinstance(value, models.Model):
model_content_type = ContentType.objects.get_for_model(value)
name = str(value)
if hasattr(value, "name"):
name = value.name
source[key] = sanitize_dict(
final_dict[key] = sanitize_dict(
{
"app": model_content_type.app_label,
"model_name": model_content_type.model,
@ -44,8 +62,10 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
}
)
elif isinstance(value, UUID):
source[key] = value.hex
return source
final_dict[key] = value.hex
else:
final_dict[key] = value
return final_dict
class EventAction(Enum):
@ -104,7 +124,7 @@ class Event(models.Model):
)
if not app:
app = getmodule(stack()[_inspect_offset][0]).__name__
cleaned_kwargs = sanitize_dict(kwargs)
cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs))
event = Event(action=action.value, app=app, context=cleaned_kwargs)
return event

View File

@ -1,5 +1,6 @@
"""passbook audit signal listener"""
from typing import Dict
from threading import Thread
from typing import Any, Dict, Optional
from django.contrib.auth.signals import (
user_logged_in,
@ -11,21 +12,54 @@ from django.http import HttpRequest
from passbook.audit.models import Event, EventAction
from passbook.core.models import User
from passbook.core.signals import invitation_created, invitation_used, user_signed_up
from passbook.stages.invitation.models import Invitation
from passbook.stages.invitation.signals import invitation_created, invitation_used
from passbook.stages.user_write.signals import user_write
class EventNewThread(Thread):
"""Create Event in background thread"""
action: EventAction
request: HttpRequest
kwargs: Dict[str, Any]
user: Optional[User] = None
def __init__(self, action: EventAction, request: HttpRequest, **kwargs):
super().__init__()
self.action = action
self.request = request
self.kwargs = kwargs
def run(self):
Event.new(self.action, **self.kwargs).from_http(self.request, user=self.user)
@receiver(user_logged_in)
# pylint: disable=unused-argument
def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
"""Log successful login"""
Event.new(EventAction.LOGIN).from_http(request)
thread = EventNewThread(EventAction.LOGIN, request)
thread.user = user
thread.run()
@receiver(user_logged_out)
# pylint: disable=unused-argument
def on_user_logged_out(sender, request: HttpRequest, user: User, **_):
"""Log successfully logout"""
Event.new(EventAction.LOGOUT).from_http(request)
thread = EventNewThread(EventAction.LOGOUT, request)
thread.user = user
thread.run()
@receiver(user_write)
# 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.user = user
thread.run()
@receiver(user_login_failed)
@ -34,29 +68,25 @@ def on_user_login_failed(
sender, credentials: Dict[str, str], request: HttpRequest, **_
):
"""Failed Login"""
Event.new(EventAction.LOGIN_FAILED, **credentials).from_http(request)
@receiver(user_signed_up)
# pylint: disable=unused-argument
def on_user_signed_up(sender, request: HttpRequest, user: User, **_):
"""Log successfully signed up"""
Event.new(EventAction.SIGN_UP).from_http(request)
thread = EventNewThread(EventAction.LOGIN_FAILED, request, **credentials)
thread.run()
@receiver(invitation_created)
# pylint: disable=unused-argument
def on_invitation_created(sender, request: HttpRequest, invitation, **_):
def on_invitation_created(sender, request: HttpRequest, invitation: Invitation, **_):
"""Log Invitation creation"""
Event.new(
EventAction.INVITE_CREATED, invitation_uuid=invitation.uuid.hex
).from_http(request)
thread = EventNewThread(
EventAction.INVITE_CREATED, request, invitation_uuid=invitation.invite_uuid.hex
)
thread.run()
@receiver(invitation_used)
# pylint: disable=unused-argument
def on_invitation_used(sender, request: HttpRequest, invitation, **_):
def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_):
"""Log Invitation usage"""
Event.new(EventAction.INVITE_USED, invitation_uuid=invitation.uuid.hex).from_http(
request
thread = EventNewThread(
EventAction.INVITE_USED, request, invitation_uuid=invitation.invite_uuid.hex
)
thread.run()

View File

@ -1,2 +1,9 @@
"""passbook audit urls"""
urlpatterns = []
from django.urls import path
from passbook.audit.views import EventListView
urlpatterns = [
# Audit Log
path("audit/", EventListView.as_view(), name="log"),
]

View File

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

View File

@ -3,12 +3,13 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from passbook.core.models import Application, Provider
from passbook.lib.widgets import GroupedModelChoiceField
class ApplicationForm(forms.ModelForm):
"""Application Form"""
provider = forms.ModelChoiceField(
provider = GroupedModelChoiceField(
queryset=Provider.objects.all().order_by("pk").select_subclasses(),
required=False,
)

View File

@ -2,6 +2,7 @@
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from passbook.admin.fields import CodeMirrorWidget, YAMLField
from passbook.core.models import Group, User
@ -34,4 +35,8 @@ class GroupForm(forms.ModelForm):
fields = ["name", "parent", "members", "attributes"]
widgets = {
"name": forms.TextInput(),
"attributes": CodeMirrorWidget,
}
field_classes = {
"attributes": YAMLField,
}

View File

@ -0,0 +1,28 @@
# Generated by Django 3.0.7 on 2020-07-03 22:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0003_default_user"),
]
operations = [
migrations.AlterModelOptions(
name="application",
options={
"verbose_name": "Application",
"verbose_name_plural": "Applications",
},
),
migrations.AlterModelOptions(
name="user",
options={
"permissions": (("reset_user_password", "Reset Password"),),
"verbose_name": "User",
"verbose_name_plural": "Users",
},
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 3.0.7 on 2020-07-05 21:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0004_auto_20200703_2213"),
]
operations = [
migrations.AddField(
model_name="token",
name="intent",
field=models.TextField(
choices=[
("verification", "Intent Verification"),
("api", "Intent Api"),
],
default="verification",
),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.0.8 on 2020-07-09 16:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0005_token_intent"),
]
operations = [
migrations.AlterField(
model_name="source",
name="slug",
field=models.SlugField(
help_text="Internal source name, used in URLs.", unique=True
),
),
]

View File

@ -1,11 +1,13 @@
"""passbook core models"""
from datetime import timedelta
from typing import Any, Optional
from typing import Any, Optional, Type
from uuid import uuid4
from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import JSONField
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.timezone import now
from django.utils.translation import gettext_lazy as _
@ -71,6 +73,8 @@ class User(GuardianUserMixin, AbstractUser):
class Meta:
permissions = (("reset_user_password", "Reset Password"),)
verbose_name = _("User")
verbose_name_plural = _("Users")
class Provider(models.Model):
@ -89,6 +93,10 @@ class Provider(models.Model):
objects = InheritanceManager()
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError
# This class defines no field for easier inheritance
def __str__(self):
if hasattr(self, "name"):
@ -112,8 +120,6 @@ class Application(PolicyBindingModel):
meta_description = models.TextField(default="", blank=True)
meta_publisher = models.TextField(default="", blank=True)
objects = InheritanceManager()
def get_provider(self) -> Optional[Provider]:
"""Get casted provider instance"""
if not self.provider:
@ -123,12 +129,19 @@ class Application(PolicyBindingModel):
def __str__(self):
return self.name
class Meta:
verbose_name = _("Application")
verbose_name_plural = _("Applications")
class Source(PolicyBindingModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
name = models.TextField(help_text=_("Source's display Name."))
slug = models.SlugField(help_text=_("Internal source name, used in URLs."))
slug = models.SlugField(
help_text=_("Internal source name, used in URLs."), unique=True
)
enabled = models.BooleanField(default=True)
property_mappings = models.ManyToManyField(
@ -154,10 +167,12 @@ class Source(PolicyBindingModel):
related_name="source_enrollment",
)
form = "" # ModelForm-based class ued to create/edit instance
objects = InheritanceManager()
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError
@property
def ui_login_button(self) -> Optional[UILoginButton]:
"""If source uses a http-based flow, return UI Information about the login
@ -190,23 +205,54 @@ class UserSourceConnection(CreatedUpdatedModel):
unique_together = (("user", "source"),)
class Token(models.Model):
"""One-time link for password resets/sign-up-confirmations"""
class ExpiringModel(models.Model):
"""Base Model which can expire, and is automatically cleaned up."""
token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
expires = models.DateTimeField(default=default_token_duration)
user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+")
expiring = models.BooleanField(default=True)
description = models.TextField(default="", blank=True)
@classmethod
def filter_not_expired(cls, **kwargs) -> QuerySet:
"""Filer for tokens which are not expired yet or are not expiring,
and match filters in `kwargs`"""
query = Q(**kwargs)
query_not_expired_yet = Q(expires__lt=now(), expiring=True)
query_not_expiring = Q(expiring=False)
return cls.objects.filter(query & (query_not_expired_yet | query_not_expiring))
@property
def is_expired(self) -> bool:
"""Check if token is expired yet."""
return now() > self.expires
class Meta:
abstract = True
class TokenIntents(models.TextChoices):
"""Intents a Token can be created for."""
# Single user token
INTENT_VERIFICATION = "verification"
# Allow access to API
INTENT_API = "api"
class Token(ExpiringModel):
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""
token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
intent = models.TextField(
choices=TokenIntents.choices, default=TokenIntents.INTENT_VERIFICATION
)
user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+")
description = models.TextField(default="", blank=True)
def __str__(self):
return (
f"Token f{self.token_uuid.hex} {self.description} (expires={self.expires})"
f"Token {self.token_uuid.hex} {self.description} (expires={self.expires})"
)
class Meta:
@ -222,9 +268,12 @@ class PropertyMapping(models.Model):
name = models.TextField()
expression = models.TextField()
form = ""
objects = InheritanceManager()
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError
def evaluate(
self, user: Optional[User], request: Optional[HttpRequest], **kwargs
) -> Any:

View File

@ -1,7 +1,4 @@
"""passbook core signals"""
from django.core.signals import Signal
user_signed_up = Signal(providing_args=["request", "user"])
invitation_created = Signal(providing_args=["request", "invitation"])
invitation_used = Signal(providing_args=["request", "invitation", "user"])
password_changed = Signal(providing_args=["user", "password"])

View File

@ -1,15 +1,16 @@
"""passbook core tasks"""
from django.utils.timezone import now
from structlog import get_logger
from passbook.core.models import Token
from passbook.core.models import ExpiringModel
from passbook.root.celery import CELERY_APP
LOGGER = get_logger()
@CELERY_APP.task()
def clean_tokens():
"""Remove expired tokens"""
amount, _ = Token.objects.filter(expires__lt=now(), expiring=True).delete()
LOGGER.debug("Deleted expired tokens", amount=amount)
def clean_expired_models():
"""Remove expired objects"""
for cls in ExpiringModel.__subclasses__():
cls: ExpiringModel
amount, _ = cls.filter_not_expired().delete()
LOGGER.debug("Deleted expired models", model=cls, amount=amount)

View File

@ -32,8 +32,8 @@
{% if user.is_superuser %}
<li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_app 'passbook_admin' %}"
href="{% url 'passbook_admin:overview' %}">{% trans 'Administrate' %}</a></li>
<li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_url 'passbook_admin:audit-log' %}"
href="{% url 'passbook_admin:audit-log' %}">{% trans 'Monitor' %}</a></li>
<li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_url 'passbook_audit:log' %}"
href="{% url 'passbook_audit:log' %}">{% trans 'Monitor' %}</a></li>
{% endif %}
</ul>
</nav>

View File

@ -20,11 +20,15 @@
</head>
<body>
{% if 'impersonate_id' in request.session %}
<div class="experimental-pf-bar">
<span id="experimentalBar" class="experimental-pf-text">
{% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %}
<a href="?__unimpersonate=True" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
</span>
<div class="pf-c-banner pf-m-warning pf-c-alert pf-m-sticky">
<div class="pf-l-flex pf-m-justify-content-center pf-m-justify-content-space-between-on-lg pf-m-nowrap" style="height: 100%;">
<div class=""></div>
<div class="pf-u-display-none pf-u-display-block-on-lg">
{% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %}
<a href="?__unimpersonate=True" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
</div>
<div class=""></div>
</div>
</div>
{% endif %}
{% block body %}

View File

@ -1,58 +1,20 @@
{% extends 'base/skeleton.html' %}
{% extends 'login/base_full.html' %}
{% load static %}
{% load i18n %}
{% load passbook_utils %}
{% block body %}
<div class="pf-c-background-image">
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
<filter id="image_overlay">
<feColorMatrix type="matrix" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0"></feColorMatrix>
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
<feFuncA type="table" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</svg>
</div>
<div class="pf-c-login">
<div class="pf-c-login__container">
<header class="pf-c-login__header">
<img class="pf-c-brand" src="{% static 'passbook/logo.svg' %}" style="height: 60px;" alt="passbook icon" />
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 60px;"
alt="passbook branding" />
</header>
<main class="pf-c-login__main" id="flow-body">
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
{% trans 'Bad Request' %}
</h1>
</header>
<div class="pf-c-login__main-body">
{% block card %}
<form method="POST" class="pf-c-form">
{% if message %}
<h3>{% trans message %}</h3>
{% endif %}
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form>
{% endblock %}
</div>
</main>
<footer class="pf-c-login__footer">
<p></p>
<ul class="pf-c-list pf-m-inline">
<li>
<a href="https://passbook.beryju.org/">{% trans 'Documentation' %}</a>
</li>
<!-- todo: load config.passbook.footer.links -->
</ul>
</footer>
</div>
</div>
{% block title %}
{% trans 'Bad Request' %}
{% endblock %}
{% block card %}
<form method="POST" class="pf-c-form">
{% if message %}
<h3>{% trans message %}</h3>
{% endif %}
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "login/base.html" %}
{% load passbook_utils %}
{% load i18n %}
{% block title %}
{{ title }}
{% endblock %}
{% block card %}
<form method="POST" action="{{ url }}" autosubmit>
{% csrf_token %}
{% for key, value in attrs.items %}
<input type="hidden" name="{{ key }}" value="{{ value }}">
{% endfor %}
<div class="pf-c-form__group pf-u-display-flex pf-u-justify-content-center">
<div class="pf-c-form__group-control">
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
</div>
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__actions">
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans 'Continue' %}</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,34 @@
{% extends "login/base_full.html" %}
{% load passbook_utils %}
{% load i18n %}
{% block title %}
{{ title }}
{% endblock %}
{% block card %}
<form method="POST" action="{{ url }}" autosubmit>
{% csrf_token %}
{% for key, value in attrs.items %}
<input type="hidden" name="{{ key }}" value="{{ value }}">
{% endfor %}
<div class="pf-c-form__group pf-u-display-flex pf-u-justify-content-center">
<div class="pf-c-form__group-control">
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
</div>
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__actions">
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans 'Continue' %}</button>
</div>
</div>
</form>
<script>
document.querySelector("form").submit();
</script>
{% endblock %}

View File

@ -0,0 +1,54 @@
{% extends 'base/skeleton.html' %}
{% load static %}
{% load i18n %}
{% load passbook_utils %}
{% block body %}
<div class="pf-c-background-image">
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
<filter id="image_overlay">
<feColorMatrix type="matrix" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0"></feColorMatrix>
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
<feFuncA type="table" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</svg>
</div>
{% include 'partials/messages.html' %}
<div class="pf-c-login">
<div class="pf-c-login__container">
<header class="pf-c-login__header">
<img class="pf-c-brand" src="{% static 'passbook/logo.svg' %}" style="height: 60px;" alt="passbook icon" />
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 60px;" alt="passbook branding" />
</header>
{% block main_container %}
<main class="pf-c-login__main">
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
{% block title %}
{% endblock %}
</h1>
</header>
<div class="pf-c-login__main-body">
{% block card %}
{% endblock %}
</div>
</main>
{% endblock %}
<footer class="pf-c-login__footer">
<p></p>
<ul class="pf-c-list pf-m-inline">
{% for link in config.passbook.footer_links %}
<li>
<a href="{{ link.href }}">{{ link.name }}</a>
</li>
{% endfor %}
</ul>
</footer>
</div>
</div>
{% endblock %}

View File

@ -1,21 +1,25 @@
{% extends 'login/base.html' %}
{% extends 'login/base_full.html' %}
{% load static %}
{% load i18n %}
{% load passbook_utils %}
{% block card %}
<form method="POST" class="pf-c-form">
{% csrf_token %}
{% include 'partials/form.html' %}
<div class="pf-c-form__group">
<p>
<i class="pf-icon pf-icon-error-circle-o"></i>
{% trans 'Access denied' %}
</p>
</div>
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form>
{% block title %}
{% trans 'Permission denied' %}
{% endblock %}
{% block card %}
<form method="POST" class="pf-c-form">
{% csrf_token %}
{% include 'partials/form.html' %}
<div class="pf-c-form__group">
<p>
<i class="pf-icon pf-icon-error-circle-o"></i>
{% trans 'Access denied' %}
</p>
</div>
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form>
{% endblock %}

View File

@ -5,16 +5,13 @@
{% block above_form %}
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
<span class="pf-c-form__label-text">{% trans "Username" %}</span>
</label>
<div class="form-control-static">
<div class="left">
<img class="pf-c-avatar" src="{% gravatar user.email %}" alt="">
{{ user.username }}
</div>
<div class="right">
<a href="{% url 'passbook_flows:default-authentication' %}">{% trans 'Not you?' %}</a>
<a href="{% url 'passbook_flows:cancel' %}">{% trans 'Not you?' %}</a>
</div>
</div>
</div>

View File

@ -24,21 +24,23 @@
{% if applications %}
<div class="pf-l-gallery pf-m-gutter">
{% for app in applications %}
<a href="{{ app.meta_launch_url }}" class="pf-c-card pf-m-hoverable pf-m-compact" id="card-1">
<div class="pf-c-card__head">
<a href="{{ app.meta_launch_url }}" class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
{% if not app.meta_icon_url %}
<i class="pf-icon pf-icon-arrow"></i>
{% else %}
<img class="app-icon pf-c-avatar" src="{{ app.meta_icon_url }}" alt="{% trans 'Application Icon' %}">
{% endif %}
</div>
<div class="pf-c-card__header pf-c-title pf-m-md">
<div class="pf-c-card__title">
<p id="card-1-check-label">{{ app.name }}</p>
<div class="pf-c-content">
<small>{{ app.meta_publisher }}</small>
</div>
</div>
<div class="pf-c-card__body">{% trans app.meta_description %}</div>
<div class="pf-c-card__body">
{% trans app.meta_description|truncatewords:35 %}
</div>
</a>
{% endfor %}
</div>
@ -50,7 +52,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 user.is_superuser %} {# TODO:use guardian permissions instead #}
<a href="{% url 'passbook_admin:application-create' %}" class="pf-c-button pf-m-primary" type="button">
{% trans 'Create Application' %}
</a>

View File

@ -15,16 +15,18 @@
</div>
<div class="pf-c-form__group-control">
{% for c in field %}
<div class="radio col-sm-10">
<input type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}" value="{{ c.data.value }}"
{% if c.data.selected %} checked {% endif %}>
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
<div class="pf-c-radio">
<input class="pf-c-radio__input"
type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}"
value="{{ c.data.value }}"
{% if c.data.selected %} checked {% endif %}/>
<label class="pf-c-radio__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
</div>
{% endfor %}
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text }}</p>
{% endif %}
{% endfor %}
</div>
{% elif field.field.widget|fieldtype == 'Select' %}
<div class="pf-c-form__group-label">

View File

@ -1,4 +1,3 @@
{% if messages %}
<ul class="pf-c-alert-group pf-m-toast">
{% for msg in messages %}
<li class="pf-c-alert-group__item">
@ -21,4 +20,3 @@
</li>
{% endfor %}
</ul>
{% endif %}

View File

@ -25,8 +25,7 @@
<ul class="pf-c-nav__list">
{% for stage in user_stages_loc %}
<li class="pf-c-nav__item">
<a href="{% url stage.view_name %}" class="pf-c-nav__link {% is_active stage.view_name %}">
<i class="{{ stage.icon }}"></i>
<a href="{{ stage.url }}" class="pf-c-nav__link {% if stage.url == request.get_full_path %} pf-m-current {% endif %}">
{{ stage.name }}
</a>
</li>
@ -41,9 +40,8 @@
<ul class="pf-c-nav__list">
{% for source in user_sources_loc %}
<li class="pf-c-nav__item">
<a href="{{ source.view_name }}"
class="pf-c-nav__link {% if user_settings.view_name == request.get_full_path %} pf-m-current {% endif %}">
<i class="{{ source.icon }}"></i>
<a href="{{ source.url }}"
class="pf-c-nav__link {% if source.url == request.get_full_path %} pf-m-current {% endif %}">
{{ source.name }}
</a>
</li>
@ -56,9 +54,11 @@
</div>
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
<section class="pf-c-page__main-section">
<div class="pf-l-split pf-m-gutter">
{% block page %}
{% endblock %}
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
{% block page %}
{% endblock %}
</div>
</div>
</section>
</main>

View File

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

View File

@ -23,7 +23,7 @@ def user_stages(context: RequestContext) -> List[UIUserSettings]:
if not user_settings:
continue
matching_stages.append(user_settings)
return matching_stages
return sorted(matching_stages, key=lambda x: x.name)
@register.simple_tag(takes_context=True)
@ -38,10 +38,8 @@ def user_sources(context: RequestContext) -> List[UIUserSettings]:
user_settings = source.ui_user_settings
if not user_settings:
continue
policy_engine = PolicyEngine(
source.policies.all(), user, context.get("request")
)
policy_engine = PolicyEngine(source, user, context.get("request"))
policy_engine.build()
if policy_engine.passing:
matching_sources.append(user_settings)
return matching_sources
return sorted(matching_sources, key=lambda x: x.name)

View File

@ -0,0 +1,18 @@
"""passbook core task tests"""
from django.test import TestCase
from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user
from passbook.core.models import Token
from passbook.core.tasks import clean_expired_models
class TestTasks(TestCase):
"""Test Tasks"""
def test_token_cleanup(self):
"""Test Token cleanup task"""
Token.objects.create(expires=now(), user=get_anonymous_user())
self.assertEqual(Token.objects.all().count(), 1)
clean_expired_models()
self.assertEqual(Token.objects.all().count(), 0)

View File

@ -8,8 +8,7 @@ class UIUserSettings:
"""Dataclass for Stage and Source's user_settings"""
name: str
icon: str
view_name: str
url: str
@dataclass

View File

@ -1,40 +0,0 @@
"""passbook access helper classes"""
from django.contrib import messages
from django.http import HttpRequest
from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import Application, Provider, User
from passbook.policies.engine import PolicyEngine
from passbook.policies.types import PolicyResult
LOGGER = get_logger()
class AccessMixin:
"""Mixin class for usage in Authorization views.
Provider functions to check application access, etc"""
# request is set by view but since this Mixin has no base class
request: HttpRequest = None
def provider_to_application(self, provider: Provider) -> Application:
"""Lookup application assigned to provider, throw error if no application assigned"""
try:
return provider.application
except Application.DoesNotExist as exc:
messages.error(
self.request,
_(
'Provider "%(name)s" has no application assigned'
% {"name": provider}
),
)
raise exc
def user_has_access(self, application: Application, user: User) -> PolicyResult:
"""Check if user has access to application."""
LOGGER.debug("Checking permissions", user=user, application=application)
policy_engine = PolicyEngine(application, user, self.request)
policy_engine.build()
return policy_engine.result

View File

@ -23,7 +23,7 @@ class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
def get_object(self):
return self.request.user
def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
unenrollment_flow = Flow.with_policy(
self.request, designation=FlowDesignation.UNRENOLLMENT

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