Compare commits

..

196 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
235 changed files with 4437 additions and 2922 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.9.0-pre5
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]

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-pre5
-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-pre5
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-pre5 \
-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-pre5
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-pre5
-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-pre5
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

View File

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

382
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:c2a223f4b48782e8b160b2130265e2a66081df111f630a5a384d6909e29a5aa9",
"sha256:ce5a4ab6af9e993d1864209cbbb6f4812f65fbc57ad6b95e5967d8bf38b1dcfb"
"sha256:35553b05b47fb6b3494bc447428342ca840348ede485e586d002399a32cae0a3",
"sha256:e47537d530d523855e52367c2ff278c732651934cd6b33daf9487649dab8e674"
],
"index": "pypi",
"version": "==1.14.16"
"version": "==1.14.33"
},
"botocore": {
"hashes": [
"sha256:99d995ef99cf77458a661f3fc64e0c3a4ce77ca30facfdf0472f44b2953dd856",
"sha256:fe0c4f7cd6b67eff3b7cb8dff6709a65d6fca10b7b7449a493b2036915e98b4c"
"sha256:273dbd8e26d4faa568e4cd4ca3180890b59ff0e3e8df7fb352796796c6808527",
"sha256:c12a0dc7021fca9d11c2bdbafdc44372e38180b56a1fab97c27b152f79455cd1"
],
"version": "==1.17.16"
"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": [
@ -232,11 +232,11 @@
},
"django-prometheus": {
"hashes": [
"sha256:054924b6aedd41f3f76940578127e7fac852a696805f956da39b7941c78bba91",
"sha256:208e55e3a11ac9edd53a3b342b033e140ffe27914af1202f0b064edf99e83fc3"
"sha256:441bd85531ecdeddacbe73c930f16de926c426869ce388fa1e8c8092f7ee5a1b",
"sha256:6e824cd407b56c01810c69d2e296940d00afe609b58818794525f9760a9a5364"
],
"index": "pypi",
"version": "==2.1.0.dev46"
"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",
@ -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": [
@ -735,11 +772,11 @@
},
"sentry-sdk": {
"hashes": [
"sha256:da06bc3641e81ec2c942f87a0676cd9180044fa3d1697524a0005345997542e2",
"sha256:e80d61af85d99a1222c1a3e2a24023618374cd50a99673aa7fa3cf920e7d813b"
"sha256:2de15b13836fa3522815a933bd9c887c77f4868071043349f94f1b896c1bcfb8",
"sha256:38bb09d0277117f76507c8728d9a5156f09a47ac5175bb8072513859d19a593b"
],
"index": "pypi",
"version": "==0.16.0"
"version": "==0.16.2"
},
"service-identity": {
"hashes": [
@ -799,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": [
@ -890,36 +927,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": [
@ -945,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": [
@ -1037,10 +1077,10 @@
},
"gitpython": {
"hashes": [
"sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a",
"sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"
"sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858",
"sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"
],
"version": "==3.1.3"
"version": "==3.1.7"
},
"idna": {
"hashes": [
@ -1127,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": [
@ -1173,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": [
@ -1235,10 +1275,10 @@
},
"stevedore": {
"hashes": [
"sha256:609912b87df5ad338ff8e44d13eaad4f4170a65b79ae9cb0aa5632598994a1b7",
"sha256:c4724f8d7b8f6be42130663855d01a9c2414d6046055b5a65ab58a0e38637688"
"sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5",
"sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"
],
"version": "==2.0.1"
"version": "==3.2.0"
},
"toml": {
"hashes": [
@ -1286,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,12 +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/azure-devops/build/beryjuorg/5d94b893-6dea-4f68-a8fe-10f1674fc3a9/1?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?
@ -21,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

View File

@ -106,7 +106,7 @@ stages:
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/docker-compose.yml'
dockerComposeFile: 'scripts/ci.docker-compose.yml'
action: 'Run services'
buildImages: false
- task: CmdLine@2
@ -117,7 +117,7 @@ stages:
- task: CmdLine@2
inputs:
script: pipenv run ./manage.py migrate
- job: coverage
- job: coverage_unittest
pool:
vmImage: 'ubuntu-latest'
steps:
@ -127,7 +127,38 @@ stages:
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/docker-compose.yml'
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
@ -138,7 +169,7 @@ stages:
- task: DockerCompose@0
displayName: Run ChromeDriver
inputs:
dockerComposeFile: 'e2e/docker-compose.yml'
dockerComposeFile: 'e2e/ci.docker-compose.yml'
action: 'Run a specific service'
serviceName: 'chrome'
- task: CmdLine@2
@ -150,28 +181,68 @@ stages:
- task: CmdLine@2
displayName: Run full test suite
inputs:
script: pipenv run coverage run ./manage.py test --failfast
- task: PublishBuildArtifacts@1
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:
PathtoPublish: 'selenium_screenshots/'
ArtifactName: 'drop'
publishLocation: 'Container'
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
codeCoverageTool: 'Cobertura'
summaryFileLocation: 'coverage.xml'
pathToSources: '$(System.DefaultWorkingDirectory)'
- task: PublishTestResults@2
condition: succeededOrFailed()
inputs:
testRunTitle: 'Publish test results for Python $(python.version)'
testResultsFiles: 'unittest.xml'
testResultsFormat: 'JUnit'
testResultsFiles: |
coverage-e2e/unittest.xml
coverage-unittest/unittest.xml
mergeTestResults: true
- task: CmdLine@2
env:
CODECOV_TOKEN: $(CODECOV_TOKEN)
@ -194,15 +265,13 @@ stages:
pool:
vmImage: 'ubuntu-latest'
steps:
- task: CmdLine@2
inputs:
script: cd gatekeeper
- task: Docker@2
inputs:
containerRegistry: 'dockerhub'
repository: 'beryju/passbook-gatekeeper'
command: 'buildAndPush'
Dockerfile: 'Dockerfile'
Dockerfile: 'gatekeeper/Dockerfile'
buildContext: 'gatekeeper/'
tags: 'gh-$(Build.SourceBranchName)'
- job: build_static
pool:
@ -211,7 +280,7 @@ stages:
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/docker-compose.yml'
dockerComposeFile: 'scripts/ci.docker-compose.yml'
action: 'Run services'
buildImages: false
- task: Docker@2

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,498 +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"
}]
}, {
"id": "1a3172e0-ac23-4781-9367-19afccee4f4a",
"name": "flows stage setup password",
"commands": [{
"id": "77784f77-d840-4b3d-a42f-7928f02fb7e1",
"comment": "",
"command": "open",
"target": "/flows/default-authentication-flow/?next=%2F",
"targets": [],
"value": ""
}, {
"id": "783aa9a6-81e5-49c6-8789-2f360a5750b1",
"comment": "",
"command": "setWindowSize",
"target": "1699x1417",
"targets": [],
"value": ""
}, {
"id": "cb0cd63e-30e9-4443-af59-5345fe26dc88",
"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": "8466ded1-c5f6-451c-b63f-0889da38503a",
"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": "pbadmin"
}, {
"id": "27383093-d01a-4416-8fc6-9caad4926cd3",
"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": "4602745a-0ebb-4425-a841-a1ed4899659d",
"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": "d1ff4f81-d8f9-45dc-ad5d-f99b54c0cd18",
"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": "014c8f57-7ef2-469c-b700-efa94ba81b66",
"comment": "",
"command": "click",
"target": "css=.pf-c-page__header",
"targets": [
["css=.pf-c-page__header", "css:finder"],
["xpath=//div[@id='page-default-nav-example']/header", "xpath:idRelative"],
["xpath=//header", "xpath:position"]
],
"value": ""
}, {
"id": "14e86b6f-6add-4bcc-913a-42b1e7322c79",
"comment": "",
"command": "click",
"target": "linkText=pbadmin",
"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": ""
}, {
"id": "8280da13-632e-4cba-9e18-ecae0d57d052",
"comment": "",
"command": "click",
"target": "linkText=Change password",
"targets": [
["linkText=Change password", "linkText"],
["css=.pf-c-nav__section:nth-child(2) .pf-c-nav__link", "css:finder"],
["xpath=//a[contains(text(),'Change password')]", "xpath:link"],
["xpath=//nav[@id='page-default-nav-example-primary-nav']/section[2]/ul/li/a", "xpath:idRelative"],
["xpath=//a[contains(@href, '/-/user/stage/password/b929b529-e384-4409-8d40-ac4a195fcab2/change/?next=%2F-%2Fuser%2F')]", "xpath:href"],
["xpath=//section[2]/ul/li/a", "xpath:position"],
["xpath=//a[contains(.,'Change password')]", "xpath:innerText"]
],
"value": ""
}, {
"id": "716d7e0c-79dc-469b-a31f-dceaa0765e9c",
"comment": "",
"command": "click",
"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/input", "xpath:idRelative"],
["xpath=//div/input", "xpath:position"]
],
"value": ""
}, {
"id": "77005d70-adf0-4add-8329-b092d43f829a",
"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/input", "xpath:idRelative"],
["xpath=//div/input", "xpath:position"]
],
"value": "test"
}, {
"id": "965ca365-99f4-45d1-97c3-c944269341b9",
"comment": "",
"command": "click",
"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[2]/input", "xpath:idRelative"],
["xpath=//div[2]/input", "xpath:position"]
],
"value": ""
}, {
"id": "9b421468-c65e-4943-b6b1-1e80410a6b87",
"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[2]/input", "xpath:idRelative"],
["xpath=//div[2]/input", "xpath:position"]
],
"value": "test"
}, {
"id": "572c1400-a0f2-499f-808a-18c1f56bf13f",
"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": ""
}]
}],
"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

@ -23,8 +23,8 @@ class TestFlowsEnroll(SeleniumTestCase):
"""Test Enroll flow"""
def setUp(self):
super().setUp()
self.container = self.setup_client()
super().setUp()
def setup_client(self) -> Container:
"""Setup test IdP container"""

View File

@ -8,6 +8,8 @@ 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):
@ -15,6 +17,16 @@ class TestFlowsStageSetup(SeleniumTestCase):
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)
@ -29,6 +41,7 @@ class TestFlowsStageSetup(SeleniumTestCase):
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()

View File

@ -20,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,
@ -103,23 +103,20 @@ class TestProviderOAuth(SeleniumTestCase):
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,
)
@ -165,6 +162,7 @@ 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")
@ -174,23 +172,20 @@ class TestProviderOAuth(SeleniumTestCase):
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,
)
@ -230,6 +225,6 @@ class TestProviderOAuth(SeleniumTestCase):
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, "#flow-body > header > h1").text,
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
"Permission denied",
)

View File

@ -23,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,
@ -153,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,
)
@ -221,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(
@ -234,23 +232,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,
)
@ -298,6 +293,6 @@ class TestProviderOIDC(SeleniumTestCase):
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, "#flow-body > header > h1").text,
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
"Permission denied",
)

View File

@ -11,7 +11,6 @@ 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 (
@ -19,7 +18,6 @@ from passbook.providers.saml.models import (
SAMLPropertyMapping,
SAMLProvider,
)
from passbook.providers.saml.processors.generic import GenericProcessor
class TestProviderSAML(SeleniumTestCase):
@ -70,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",
@ -104,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",
@ -146,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",
@ -188,7 +183,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",
@ -211,6 +205,6 @@ class TestProviderSAML(SeleniumTestCase):
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, "#flow-body > header > h1").text,
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
@ -46,8 +46,8 @@ class SeleniumTestCase(StaticLiveServerTestCase):
makedirs("selenium_screenshots/", exist_ok=True)
self.driver = self._get_driver()
self.driver.maximize_window()
self.driver.implicitly_wait(300)
self.wait = WebDriverWait(self.driver, 500)
self.driver.implicitly_wait(30)
self.wait = WebDriverWait(self.driver, 50)
self.apply_default_data()
self.logger = get_logger()
@ -58,9 +58,12 @@ class SeleniumTestCase(StaticLiveServerTestCase):
)
def tearDown(self):
self.driver.save_screenshot(
f"selenium_screenshots/{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"]

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-pre5"
appVersion: "0.9.0-stable"
description: A Helm chart for passbook.
name: passbook
version: "0.9.0-pre5"
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-pre5
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-pre5"
__version__ = "0.9.0-stable"

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,58 +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">
<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 %}
<i class="pf-icon pf-icon-ok"></i>
{% blocktrans with version=version %}
{{ version }} (Up-to-date!)
{% blocktrans %}
Up-to-date!
{% endblocktrans %}
{% else %}
<i class="pf-icon pf-icon-warning-triangle"></i>
{% blocktrans with version=version latest=version_latest %}
{{ version }} ({{ latest }} is available!)
{% 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' %}
@ -142,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' %}
@ -158,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>
@ -173,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

@ -69,12 +69,11 @@
<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 Applications.' %}
{% trans 'No Tokens.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no applications exist. Click the button below to create one.' %}
{% trans 'Currently no tokens exist.' %}
</div>
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
</div>
{% endif %}

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,6 +1,4 @@
"""passbook administration overview"""
from functools import lru_cache
from django.core.cache import cache
from django.shortcuts import redirect, reverse
from django.views.generic import TemplateView
@ -15,18 +13,21 @@ 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"
@lru_cache
def latest_version() -> Version:
"""Get latest release from GitHub, cached"""
try:
data = get(
"https://api.github.com/repos/beryju/passbook/releases/latest"
).json()
tag_name = data.get("tag_name")
return parse(tag_name.split("/")[1])
except RequestException:
return parse("0.0.0")
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):
@ -60,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

@ -11,10 +11,10 @@ from django.views.generic import ListView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.core.signals import invitation_created
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):

View File

@ -6,7 +6,7 @@ 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, path_to_class
from passbook.lib.utils.reflection import all_subclasses
from passbook.lib.views import CreateAssignPermView
@ -40,7 +40,7 @@ class InheritanceCreateView(CreateAssignPermView):
)
except StopIteration as exc:
raise Http404 from exc
return path_to_class(model.form)
return model.form(model)
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
@ -61,9 +61,7 @@ class InheritanceUpdateView(UpdateView):
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
return self.get_object().form()
def get_object(self, queryset=None):
return (

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,11 +1,12 @@
"""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

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,12 +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 _
@ -92,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"):
@ -134,7 +139,9 @@ 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(
@ -160,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
@ -196,6 +205,31 @@ class UserSourceConnection(CreatedUpdatedModel):
unique_together = (("user", "source"),)
class ExpiringModel(models.Model):
"""Base Model which can expire, and is automatically cleaned up."""
expires = models.DateTimeField(default=default_token_duration)
expiring = models.BooleanField(default=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."""
@ -206,34 +240,16 @@ class TokenIntents(models.TextChoices):
INTENT_API = "api"
class Token(models.Model):
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
)
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)
@staticmethod
def filter_not_expired(**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 Token.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
def __str__(self):
return (
f"Token {self.token_uuid.hex} {self.description} (expires={self.expires})"
@ -252,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

@ -1,57 +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,62 +1,25 @@
{% 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 'Permission denied' %}
</h1>
</header>
<div class="pf-c-login__main-body">
{% 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 %}
</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 '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

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

@ -40,7 +40,7 @@
<ul class="pf-c-nav__list">
{% for source in user_sources_loc %}
<li class="pf-c-nav__item">
<a href="{{ source.view_name }}"
<a href="{{ source.url }}"
class="pf-c-nav__link {% if source.url == request.get_full_path %} pf-m-current {% endif %}">
{{ source.name }}
</a>

View File

@ -1,10 +1,10 @@
"""passbook user view tests"""
"""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_tokens
from passbook.core.tasks import clean_expired_models
class TestTasks(TestCase):
@ -14,5 +14,5 @@ class TestTasks(TestCase):
"""Test Token cleanup task"""
Token.objects.create(expires=now(), user=get_anonymous_user())
self.assertEqual(Token.objects.all().count(), 1)
clean_tokens()
clean_expired_models()
self.assertEqual(Token.objects.all().count(), 0)

View File

@ -56,7 +56,9 @@ class CertificateKeyPair(CreatedUpdatedModel):
@property
def fingerprint(self) -> str:
"""Get SHA256 Fingerprint of certificate_data"""
return hexlify(self.certificate.fingerprint(hashes.SHA256())).decode("utf-8")
return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode(
"utf-8"
)
def __str__(self) -> str:
return f"Certificate-Key Pair {self.name} {self.fingerprint}"

View File

@ -10,9 +10,9 @@ from passbook.stages.prompt.models import FieldTypes
FLOW_POLICY_EXPRESSION = """# This policy ensures that this flow can only be used when the user
# is in a SSO Flow (meaning they come from an external IdP)
return pb_is_sso_flow"""
PROMPT_POLICY_EXPRESSION = """# Check if we've been given a username by the external IdP
PROMPT_POLICY_EXPRESSION = """# Check if we've not been given a username by the external IdP
# and trigger the enrollment flow
return 'username' in pb_flow_plan.context.get('prompt_data', {})"""
return 'username' not in pb_flow_plan.context.get('prompt_data', {})"""
def create_default_source_enrollment_flow(

View File

@ -1,17 +1,20 @@
"""Flow models"""
from typing import Callable, Optional
from typing import TYPE_CHECKING, Optional, Type
from uuid import uuid4
from django.db import models
from django.forms import ModelForm
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from model_utils.managers import InheritanceManager
from structlog import get_logger
from passbook.core.types import UIUserSettings
from passbook.lib.utils.reflection import class_to_path
from passbook.policies.models import PolicyBindingModel
if TYPE_CHECKING:
from passbook.flows.stage import StageView
LOGGER = get_logger()
@ -44,8 +47,17 @@ class Stage(models.Model):
name = models.TextField()
objects = InheritanceManager()
type = ""
form = ""
def type(self) -> Type["StageView"]:
"""Return StageView class that implements logic for this stage"""
# This is a bit of a workaround, since we can't set class methods with setattr
if hasattr(self, "__in_memory_type"):
return getattr(self, "__in_memory_type")
raise NotImplementedError
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError
@property
def ui_user_settings(self) -> Optional[UIUserSettings]:
@ -57,11 +69,13 @@ class Stage(models.Model):
return f"Stage {self.name}"
def in_memory_stage(_type: Callable) -> Stage:
def in_memory_stage(view: Type["StageView"]) -> Stage:
"""Creates an in-memory stage instance, based on a `_type` as view."""
class_path = class_to_path(_type)
stage = Stage()
stage.type = class_path
# Because we can't pickle a locally generated function,
# we set the view as a separate property and reference a generic function
# that returns that member
setattr(stage, "__in_memory_type", view)
return stage

View File

@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional
from django.core.cache import cache
from django.http import HttpRequest
from elasticapm import capture_span
from structlog import get_logger
from passbook.core.models import User
@ -51,7 +52,8 @@ class FlowPlan:
stage = self.stages[0]
marker = self.markers[0]
LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker)
if marker.__class__ is not StageMarker:
LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker)
marked_stage = marker.process(self, stage)
if not marked_stage:
LOGGER.debug("f(plan_inst): marker returned none, next stage", stage=stage)
@ -88,6 +90,7 @@ class FlowPlanner:
self.allow_empty_flows = False
self.flow = flow
@capture_span(name="FlowPlanner", span_type="flow.planner.plan")
def plan(
self, request: HttpRequest, default_context: Optional[Dict[str, Any]] = None
) -> FlowPlan:
@ -127,6 +130,7 @@ class FlowPlanner:
raise EmptyFlowException()
return plan
@capture_span(name="FlowPlanner", span_type="flow.planner.build_plan")
def _build_plan(
self,
user: User,

View File

@ -1,4 +1,4 @@
{% extends 'base/skeleton.html' %}
{% extends 'login/base_full.html' %}
{% load static %}
{% load i18n %}
@ -20,50 +20,16 @@
</style>
{% endblock %}
{% 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>
<ul class="pf-c-alert-group pf-m-toast">
</ul>
<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">
<div class="pf-c-login__main-body pb-loading">
<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>
</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>
{% block main_container %}
<main class="pf-c-login__main" id="flow-body">
<div class="pf-c-login__main-body pb-loading">
<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>
</main>
<script>
const flowBodyUrl = "{{ exec_url }}";
const messagesUrl = "{{ msg_url }}";
@ -171,6 +137,7 @@ const setFormSubmitHandlers = () => {
form.addEventListener('submit', (e) => {
e.preventDefault();
let formData = new FormData(form);
showSpinner();
fetch(flowBodyUrl, {
method: 'post',
body: formData,

View File

@ -21,14 +21,15 @@ from passbook.core.views.utils import PermissionDeniedView
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from passbook.flows.models import Flow, FlowDesignation, Stage
from passbook.flows.planner import FlowPlan, FlowPlanner
from passbook.lib.utils.reflection import class_to_path, path_to_class
from passbook.lib.utils.urls import redirect_with_qs
from passbook.lib.utils.reflection import class_to_path
from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs
from passbook.lib.views import bad_request_message
LOGGER = get_logger()
# Argument used to redirect user after login
NEXT_ARG_NAME = "next"
SESSION_KEY_PLAN = "passbook_flows_plan"
SESSION_KEY_APPLICATION_PRE = "passbook_flows_application_pre"
SESSION_KEY_GET = "passbook_flows_get"
@ -49,8 +50,9 @@ class FlowExecutorView(View):
def handle_invalid_flow(self, exc: BaseException) -> HttpResponse:
"""When a flow is non-applicable check if user is on the correct domain"""
if NEXT_ARG_NAME in self.request.GET:
LOGGER.debug("f(exec): Redirecting to next on fail")
return redirect(self.request.GET.get(NEXT_ARG_NAME))
if not is_url_absolute(self.request.GET.get(NEXT_ARG_NAME)):
LOGGER.debug("f(exec): Redirecting to next on fail")
return redirect(self.request.GET.get(NEXT_ARG_NAME))
message = exc.__doc__ if exc.__doc__ else str(exc)
return bad_request_message(self.request, message)
@ -92,7 +94,7 @@ class FlowExecutorView(View):
if not self.current_stage:
LOGGER.debug("f(exec): no more stages, flow is done.")
return self._flow_done()
stage_cls = path_to_class(self.current_stage.type)
stage_cls = self.current_stage.type()
self.current_stage_view = stage_cls(self)
self.current_stage_view.args = self.args
self.current_stage_view.kwargs = self.kwargs
@ -151,11 +153,12 @@ class FlowExecutorView(View):
def _flow_done(self) -> HttpResponse:
"""User Successfully passed all stages"""
self.cancel()
# Since this is wrapped by the ExecutorShell, the next argument is saved in the session
# extract the next param before cancel as that cleans it
next_param = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "passbook_core:overview"
)
self.cancel()
return redirect_with_qs(next_param)
def stage_ok(self) -> HttpResponse:
@ -198,8 +201,14 @@ class FlowExecutorView(View):
def cancel(self):
"""Cancel current execution and return a redirect"""
if SESSION_KEY_PLAN in self.request.session:
del self.request.session[SESSION_KEY_PLAN]
keys_to_delete = [
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_PLAN,
SESSION_KEY_GET,
]
for key in keys_to_delete:
if key in self.request.session:
del self.request.session[key]
class FlowPermissionDeniedView(PermissionDeniedView):

View File

@ -3,11 +3,12 @@ import os
from collections.abc import Mapping
from contextlib import contextmanager
from glob import glob
from typing import Any
from typing import Any, Dict
from urllib.parse import urlparse
import yaml
from django.conf import ImproperlyConfigured
from django.http import HttpRequest
from structlog import get_logger
SEARCH_PATHS = ["passbook/lib/default.yml", "/etc/passbook/config.yml", ""] + glob(
@ -18,6 +19,12 @@ ENV_PREFIX = "PASSBOOK"
ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local")
def context_processor(request: HttpRequest) -> Dict[str, Any]:
"""Context Processor that injects config object into every template"""
kwargs = {"config": CONFIG.raw}
return kwargs
class ConfigLoader:
"""Search through SEARCH_PATHS and load configuration. Environment variables starting with
`ENV_PREFIX` are also applied.

View File

@ -18,7 +18,9 @@ log_level: warning
error_reporting: false
passbook:
# Optionally add links to the footer on the login page
footer_links:
# Optionally add links to the footer on the login page
- name: Documentation
href: https://passbook.beryju.org/
# - name: test
# href: https://test

View File

@ -4,6 +4,7 @@ from textwrap import indent
from typing import Any, Dict, Iterable, Optional
from django.core.exceptions import ValidationError
from elasticapm import capture_span
from requests import Session
from structlog import get_logger
@ -63,11 +64,14 @@ class BaseEvaluator:
def wrap_expression(self, expression: str, params: Iterable[str]) -> str:
"""Wrap expression in a function, call it, and save the result as `result`"""
handler_signature = ",".join(params)
full_expression = f"def handler({handler_signature}):\n"
full_expression = ""
full_expression += "from ipaddress import ip_address, ip_network\n"
full_expression += f"def handler({handler_signature}):\n"
full_expression += indent(expression, " ")
full_expression += f"\nresult = handler({handler_signature})"
return full_expression
@capture_span(name="BaseEvaluator", span_type="lib.evaluator.evaluate")
def evaluate(self, expression_source: str) -> Any:
"""Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
If any exception is raised during execution, it is raised.

View File

@ -4,6 +4,7 @@ from botocore.client import ClientError
from django.core.exceptions import DisallowedHost, ValidationError
from django.db import InternalError, OperationalError, ProgrammingError
from django_redis.exceptions import ConnectionInterrupted
from elasticapm.transport.http import TransportException
from redis.exceptions import RedisError
from rest_framework.exceptions import APIException
from structlog import get_logger
@ -33,9 +34,10 @@ def before_send(event, hint):
OSError,
RedisError,
SentryIgnoredException,
TransportException,
)
if "exc_info" in hint:
_exc_type, exc_value, _ = hint["exc_info"]
_, exc_value, _ = hint["exc_info"]
if isinstance(exc_value, ignored_classes):
LOGGER.info("Supressing error %r", exc_value)
return None

View File

@ -36,7 +36,7 @@ def back(context: Context) -> str:
def fieldtype(field):
"""Return classname"""
if isinstance(field.__class__, Model) or issubclass(field.__class__, Model):
return field._meta.verbose_name
return verbose_name(field)
return field.__class__.__name__
@ -84,6 +84,9 @@ def verbose_name(obj) -> str:
"""Return Object's Verbose Name"""
if not obj:
return ""
if hasattr(obj, "verbose_name"):
print(obj.verbose_name)
return obj.verbose_name
return obj._meta.verbose_name
@ -92,7 +95,7 @@ def form_verbose_name(obj) -> str:
"""Return ModelForm's Object's Verbose Name"""
if not obj:
return ""
return obj._meta.model._meta.verbose_name
return verbose_name(obj._meta.model)
@register.filter

View File

@ -23,4 +23,4 @@ def get_client_ip(request: Optional[HttpRequest]) -> Optional[str]:
Returns none if no IP Could be found"""
if request:
return _get_client_ip_from_meta(request.META)
return ""
return None

View File

@ -0,0 +1,38 @@
"""Time utilities"""
import datetime
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
ALLOWED_KEYS = (
"days",
"seconds",
"microseconds",
"milliseconds",
"minutes",
"hours",
"weeks",
)
def timedelta_string_validator(value: str):
"""Validator for Django that checks if value can be parsed with `timedelta_from_string`"""
try:
timedelta_from_string(value)
except ValueError as exc:
raise ValidationError(
_("%(value)s is not in the correct format of 'hours=3;minutes=1'."),
params={"value": value},
) from exc
def timedelta_from_string(expr: str) -> datetime.timedelta:
"""Convert a string with the format of 'hours=1;minute=3;seconds=5' to a
`datetime.timedelta` Object with hours = 1, minutes = 3, seconds = 5"""
kwargs = {}
for duration_pair in expr.split(";"):
key, value = duration_pair.split("=")
if key.lower() not in ALLOWED_KEYS:
continue
kwargs[key.lower()] = float(value)
return datetime.timedelta(**kwargs)

View File

@ -1,8 +1,10 @@
"""Dummy policy"""
from random import SystemRandom
from time import sleep
from typing import Type
from django.db import models
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from structlog import get_logger
@ -22,7 +24,10 @@ class DummyPolicy(Policy):
wait_min = models.IntegerField(default=5)
wait_max = models.IntegerField(default=30)
form = "passbook.policies.dummy.forms.DummyPolicyForm"
def form(self) -> Type[ModelForm]:
from passbook.policies.dummy.forms import DummyPolicyForm
return DummyPolicyForm
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Wait random time then return result"""

View File

@ -5,6 +5,7 @@ from typing import List, Optional
from django.core.cache import cache
from django.http import HttpRequest
from elasticapm import capture_span
from structlog import get_logger
from passbook.core.models import User
@ -69,6 +70,7 @@ class PolicyEngine:
if policy.__class__ == Policy:
raise TypeError(f"Policy '{policy}' is root type")
@capture_span(name="PolicyEngine", span_type="policy.engine.build")
def build(self) -> "PolicyEngine":
"""Build task group"""
for binding in self._iter_bindings():

View File

@ -1,7 +1,9 @@
"""passbook password_expiry_policy Models"""
from datetime import timedelta
from typing import Type
from django.db import models
from django.forms import ModelForm
from django.utils.timezone import now
from django.utils.translation import gettext as _
from structlog import get_logger
@ -19,7 +21,10 @@ class PasswordExpiryPolicy(Policy):
deny_only = models.BooleanField(default=False)
days = models.IntegerField()
form = "passbook.policies.expiry.forms.PasswordExpiryPolicyForm"
def form(self) -> Type[ModelForm]:
from passbook.policies.expiry.forms import PasswordExpiryPolicyForm
return PasswordExpiryPolicyForm
def passes(self, request: PolicyRequest) -> PolicyResult:
"""If password change date is more than x days in the past, call set_unusable_password

View File

@ -1,4 +1,5 @@
"""passbook expression policy evaluator"""
from ipaddress import ip_address
from typing import List
from django.http import HttpRequest
@ -41,7 +42,9 @@ class PolicyEvaluator(BaseEvaluator):
"""Update context based on http request"""
# update passbook/policies/expression/templates/policy/expression/form.html
# update docs/policies/expression/index.md
self._context["pb_client_ip"] = get_client_ip(request) or "255.255.255.255"
self._context["pb_client_ip"] = ip_address(
get_client_ip(request) or "255.255.255.255"
)
self._context["request"] = request
if SESSION_KEY_PLAN in request.session:
self._context["pb_flow_plan"] = request.session[SESSION_KEY_PLAN]

View File

@ -1,5 +1,8 @@
"""passbook expression Policy Models"""
from typing import Type
from django.db import models
from django.forms import ModelForm
from django.utils.translation import gettext as _
from passbook.policies.expression.evaluator import PolicyEvaluator
@ -12,7 +15,10 @@ class ExpressionPolicy(Policy):
expression = models.TextField()
form = "passbook.policies.expression.forms.ExpressionPolicyForm"
def form(self) -> Type[ModelForm]:
from passbook.policies.expression.forms import ExpressionPolicyForm
return ExpressionPolicyForm
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Evaluate and render expression. Returns PolicyResult(false) on error."""

View File

@ -36,7 +36,7 @@ class TestEvaluator(TestCase):
evaluator.set_policy_request(self.request)
result = evaluator.evaluate(template)
self.assertEqual(result.passing, False)
self.assertEqual(result.messages, ("invalid syntax (test, line 2)",))
self.assertEqual(result.messages, ("invalid syntax (test, line 3)",))
def test_undefined(self):
"""test undefined result"""

View File

@ -1,5 +1,8 @@
"""user field matcher models"""
from typing import Type
from django.db import models
from django.forms import ModelForm
from django.utils.translation import gettext as _
from passbook.core.models import Group
@ -12,7 +15,10 @@ class GroupMembershipPolicy(Policy):
group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL)
form = "passbook.policies.group_membership.forms.GroupMembershipPolicyForm"
def form(self) -> Type[ModelForm]:
from passbook.policies.group_membership.forms import GroupMembershipPolicyForm
return GroupMembershipPolicyForm
def passes(self, request: PolicyRequest) -> PolicyResult:
return PolicyResult(self.group.user_set.filter(pk=request.user.pk).exists())

View File

@ -11,7 +11,7 @@ class HaveIBeenPwendPolicySerializer(ModelSerializer):
class Meta:
model = HaveIBeenPwendPolicy
fields = GENERAL_SERIALIZER_FIELDS + ["allowed_count"]
fields = GENERAL_SERIALIZER_FIELDS + ["password_field", "allowed_count"]
class HaveIBeenPwendPolicyViewSet(ModelViewSet):

View File

@ -14,9 +14,9 @@ class HaveIBeenPwnedPolicyForm(forms.ModelForm):
class Meta:
model = HaveIBeenPwendPolicy
fields = GENERAL_FIELDS + ["allowed_count"]
fields = GENERAL_FIELDS + ["password_field", "allowed_count"]
widgets = {
"name": forms.TextInput(),
"order": forms.NumberInput(),
"password_field": forms.TextInput(),
"policies": FilteredSelectMultiple(_("policies"), False),
}

View File

@ -0,0 +1,21 @@
# Generated by Django 3.0.8 on 2020-07-10 18:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_policies_hibp", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="haveibeenpwendpolicy",
name="password_field",
field=models.TextField(
default="password",
help_text="Field key to check, field keys defined in Prompt stages are available.",
),
),
]

View File

@ -1,13 +1,15 @@
"""passbook HIBP Models"""
from hashlib import sha1
from typing import Type
from django.db import models
from django.forms import ModelForm
from django.utils.translation import gettext as _
from requests import get
from structlog import get_logger
from passbook.core.models import User
from passbook.policies.models import Policy, PolicyResult
from passbook.policies.types import PolicyRequest
LOGGER = get_logger()
@ -16,20 +18,34 @@ class HaveIBeenPwendPolicy(Policy):
"""Check if password is on HaveIBeenPwned's list by uploading the first
5 characters of the SHA1 Hash."""
password_field = models.TextField(
default="password",
help_text=_(
"Field key to check, field keys defined in Prompt stages are available."
),
)
allowed_count = models.IntegerField(default=0)
form = "passbook.policies.hibp.forms.HaveIBeenPwnedPolicyForm"
def form(self) -> Type[ModelForm]:
from passbook.policies.hibp.forms import HaveIBeenPwnedPolicyForm
def passes(self, user: User) -> PolicyResult:
return HaveIBeenPwnedPolicyForm
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Check if password is in HIBP DB. Hashes given Password with SHA1, uses the first 5
characters of Password in request and checks if full hash is in response. Returns 0
if Password is not in result otherwise the count of how many times it was used."""
# Only check if password is being set
if not hasattr(user, "__password__"):
return PolicyResult(True)
password = getattr(user, "__password__")
if self.password_field not in request.context:
LOGGER.warning(
"Password field not set in Policy Request",
field=self.password_field,
fields=request.context.keys(),
)
password = request.context[self.password_field]
pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec
url = "https://api.pwnedpasswords.com/range/%s" % pw_hash[:5]
url = f"https://api.pwnedpasswords.com/range/{pw_hash[:5]}"
result = get(url).text
final_count = 0
for line in result.split("\r\n"):

View File

@ -0,0 +1,29 @@
"""HIBP Policy tests"""
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from oauth2_provider.generators import generate_client_secret
from passbook.policies.hibp.models import HaveIBeenPwendPolicy
from passbook.policies.types import PolicyRequest, PolicyResult
class TestHIBPPolicy(TestCase):
"""Test HIBP Policy"""
def test_false(self):
"""Failing password case"""
policy = HaveIBeenPwendPolicy.objects.create(name="test_false",)
request = PolicyRequest(get_anonymous_user())
request.context["password"] = "password"
result: PolicyResult = policy.passes(request)
self.assertFalse(result.passing)
self.assertTrue(result.messages[0].startswith("Password exists on "))
def test_true(self):
"""Positive password case"""
policy = HaveIBeenPwendPolicy.objects.create(name="test_true",)
request = PolicyRequest(get_anonymous_user())
request.context["password"] = generate_client_secret()
result: PolicyResult = policy.passes(request)
self.assertTrue(result.passing)
self.assertEqual(result.messages, tuple())

View File

@ -3,12 +3,14 @@ from typing import Optional
from django.contrib import messages
from django.contrib.auth.mixins import AccessMixin
from django.contrib.auth.views import redirect_to_login
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import Application, Provider, User
from passbook.flows.views import SESSION_KEY_APPLICATION_PRE
from passbook.policies.engine import PolicyEngine
from passbook.policies.types import PolicyResult
@ -25,8 +27,18 @@ class PolicyAccessMixin(BaseMixin, AccessMixin):
"""Mixin class for usage in Authorization views.
Provider functions to check application access, etc"""
def handle_no_permission(self, application: Optional[Application] = None):
if application:
self.request.session[SESSION_KEY_APPLICATION_PRE] = application
return redirect_to_login(
self.request.get_full_path(),
self.get_login_url(),
self.get_redirect_field_name(),
)
def handle_no_permission_authorized(self) -> HttpResponse:
"""Function called when user has no permissions but is authorized"""
# TODO: Remove this URL and render the view instead
return redirect("passbook_flows:denied")
def provider_to_application(self, provider: Provider) -> Application:

View File

@ -1,7 +1,9 @@
"""Policy base models"""
from typing import Type
from uuid import uuid4
from django.db import models
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from model_utils.managers import InheritanceManager
@ -73,6 +75,10 @@ class Policy(CreatedUpdatedModel):
objects = InheritanceAutoManager()
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError
def __str__(self):
return f"Policy {self.name}"

View File

@ -12,6 +12,7 @@ class PasswordPolicySerializer(ModelSerializer):
class Meta:
model = PasswordPolicy
fields = GENERAL_SERIALIZER_FIELDS + [
"password_field",
"amount_uppercase",
"amount_lowercase",
"amount_symbols",

View File

@ -14,6 +14,7 @@ class PasswordPolicyForm(forms.ModelForm):
model = PasswordPolicy
fields = GENERAL_FIELDS + [
"password_field",
"amount_uppercase",
"amount_lowercase",
"amount_symbols",
@ -23,6 +24,7 @@ class PasswordPolicyForm(forms.ModelForm):
]
widgets = {
"name": forms.TextInput(),
"password_field": forms.TextInput(),
"symbol_charset": forms.TextInput(),
"error_message": forms.TextInput(),
}

View File

@ -0,0 +1,21 @@
# Generated by Django 3.0.8 on 2020-07-10 18:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_policies_password", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="passwordpolicy",
name="password_field",
field=models.TextField(
default="password",
help_text="Field key to check, field keys defined in Prompt stages are available.",
),
),
]

View File

@ -1,7 +1,9 @@
"""user field matcher models"""
import re
from typing import Type
from django.db import models
from django.forms import ModelForm
from django.utils.translation import gettext as _
from structlog import get_logger
@ -14,6 +16,13 @@ LOGGER = get_logger()
class PasswordPolicy(Policy):
"""Policy to make sure passwords have certain properties"""
password_field = models.TextField(
default="password",
help_text=_(
"Field key to check, field keys defined in Prompt stages are available."
),
)
amount_uppercase = models.IntegerField(default=0)
amount_lowercase = models.IntegerField(default=0)
amount_symbols = models.IntegerField(default=0)
@ -21,22 +30,35 @@ class PasswordPolicy(Policy):
symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
error_message = models.TextField()
form = "passbook.policies.password.forms.PasswordPolicyForm"
def form(self) -> Type[ModelForm]:
from passbook.policies.password.forms import PasswordPolicyForm
return PasswordPolicyForm
def passes(self, request: PolicyRequest) -> PolicyResult:
# Only check if password is being set
if not hasattr(request.user, "__password__"):
return PolicyResult(True)
password = getattr(request.user, "__password__")
if self.password_field not in request.context:
LOGGER.warning(
"Password field not set in Policy Request",
field=self.password_field,
fields=request.context.keys(),
)
password = request.context[self.password_field]
filter_regex = r""
filter_regex = []
if self.amount_lowercase > 0:
filter_regex += r"[a-z]{%d,}" % self.amount_lowercase
filter_regex.append(r"[a-z]{%d,}" % self.amount_lowercase)
if self.amount_uppercase > 0:
filter_regex += r"[A-Z]{%d,}" % self.amount_uppercase
filter_regex.append(r"[A-Z]{%d,}" % self.amount_uppercase)
if self.amount_symbols > 0:
filter_regex += r"[%s]{%d,}" % (self.symbol_charset, self.amount_symbols)
result = bool(re.compile(filter_regex).match(password))
filter_regex.append(
r"[%s]{%d,}" % (self.symbol_charset, self.amount_symbols)
)
full_regex = "|".join(filter_regex)
LOGGER.debug("Built regex", regexp=full_regex)
result = bool(re.compile(full_regex).match(password))
result = result and len(password) >= self.length_min
if not result:
return PolicyResult(result, self.error_message)
return PolicyResult(result)

View File

@ -0,0 +1,42 @@
"""Password Policy tests"""
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from passbook.policies.password.models import PasswordPolicy
from passbook.policies.types import PolicyRequest, PolicyResult
class TestPasswordPolicy(TestCase):
"""Test Password Policy"""
def test_false(self):
"""Failing password case"""
policy = PasswordPolicy.objects.create(
name="test_false",
amount_uppercase=1,
amount_lowercase=2,
amount_symbols=3,
length_min=24,
error_message="test message",
)
request = PolicyRequest(get_anonymous_user())
request.context["password"] = "test"
result: PolicyResult = policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages, ("test message",))
def test_true(self):
"""Positive password case"""
policy = PasswordPolicy.objects.create(
name="test_true",
amount_uppercase=1,
amount_lowercase=2,
amount_symbols=3,
length_min=3,
error_message="test message",
)
request = PolicyRequest(get_anonymous_user())
request.context["password"] = "Test()!"
result: PolicyResult = policy.passes(request)
self.assertTrue(result.passing)
self.assertEqual(result.messages, tuple())

View File

@ -4,6 +4,7 @@ from multiprocessing.connection import Connection
from typing import Optional
from django.core.cache import cache
from elasticapm import capture_span
from structlog import get_logger
from passbook.policies.exceptions import PolicyException
@ -44,6 +45,7 @@ class PolicyProcess(Process):
if connection:
self.connection = connection
@capture_span(name="PolicyEngine", span_type="policy.process.execute")
def execute(self) -> PolicyResult:
"""Run actual policy, returns result"""
LOGGER.debug(

View File

@ -1,5 +1,9 @@
"""passbook reputation request policy"""
from typing import Type
from django.core.cache import cache
from django.db import models
from django.forms import ModelForm
from django.utils.translation import gettext as _
from passbook.core.models import User
@ -7,6 +11,9 @@ from passbook.lib.utils.http import get_client_ip
from passbook.policies.models import Policy
from passbook.policies.types import PolicyRequest, PolicyResult
CACHE_KEY_IP_PREFIX = "passbook_reputation_ip_"
CACHE_KEY_USER_PREFIX = "passbook_reputation_user_"
class ReputationPolicy(Policy):
"""Return true if request IP/target username's score is below a certain threshold"""
@ -15,21 +22,20 @@ class ReputationPolicy(Policy):
check_username = models.BooleanField(default=True)
threshold = models.IntegerField(default=-5)
form = "passbook.policies.reputation.forms.ReputationPolicyForm"
def form(self) -> Type[ModelForm]:
from passbook.policies.reputation.forms import ReputationPolicyForm
return ReputationPolicyForm
def passes(self, request: PolicyRequest) -> PolicyResult:
remote_ip = get_client_ip(request.http_request)
remote_ip = get_client_ip(request.http_request) or "255.255.255.255"
passing = True
if self.check_ip:
ip_scores = IPReputation.objects.filter(
ip=remote_ip, score__lte=self.threshold
)
passing = passing and ip_scores.exists()
score = cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0)
passing = passing and score <= self.threshold
if self.check_username:
user_scores = UserReputation.objects.filter(
user=request.user, score__lte=self.threshold
)
passing = passing and user_scores.exists()
score = cache.get_or_set(CACHE_KEY_USER_PREFIX + request.user.username, 0)
passing = passing and score <= self.threshold
return PolicyResult(passing)
class Meta:

View File

@ -0,0 +1,13 @@
"""Reputation Settings"""
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
"policies_reputation_ip_save": {
"task": "passbook.policies.reputation.tasks.save_ip_reputation",
"schedule": crontab(minute="*/5"),
},
"policies_reputation_user_save": {
"task": "passbook.policies.reputation.tasks.save_user_reputation",
"schedule": crontab(minute="*/5"),
},
}

View File

@ -1,36 +1,39 @@
"""passbook reputation request signals"""
from django.contrib.auth.signals import user_logged_in, user_login_failed
from django.core.cache import cache
from django.dispatch import receiver
from django.http import HttpRequest
from structlog import get_logger
from passbook.core.models import User
from passbook.lib.utils.http import get_client_ip
from passbook.policies.reputation.models import IPReputation, UserReputation
from passbook.policies.reputation.models import (
CACHE_KEY_IP_PREFIX,
CACHE_KEY_USER_PREFIX,
)
LOGGER = get_logger()
def update_score(request, username, amount):
def update_score(request: HttpRequest, username: str, amount: int):
"""Update score for IP and User"""
remote_ip = get_client_ip(request) or "255.255.255.255."
ip_score, _ = IPReputation.objects.update_or_create(ip=remote_ip)
ip_score.score += amount
ip_score.save()
LOGGER.debug("Updated score", amount=amount, for_ip=remote_ip)
user = User.objects.filter(username=username)
if not user.exists():
return
user_score, _ = UserReputation.objects.update_or_create(user=user.first())
user_score.score += amount
user_score.save()
LOGGER.debug("Updated score", amount=amount, for_user=username)
remote_ip = get_client_ip(request) or "255.255.255.255"
# We only update the cache here, as its faster than writing to the DB
cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0)
cache.incr(CACHE_KEY_IP_PREFIX + remote_ip, amount)
cache.get_or_set(CACHE_KEY_USER_PREFIX + username, 0)
cache.incr(CACHE_KEY_USER_PREFIX + username, amount)
LOGGER.debug("Updated score", amount=amount, for_user=username, for_ip=remote_ip)
@receiver(user_login_failed)
# pylint: disable=unused-argument
def handle_failed_login(sender, request, credentials, **_):
"""Lower Score for failed loging attempts"""
update_score(request, credentials.get("username"), -1)
if "username" in credentials:
update_score(request, credentials.get("username"), -1)
@receiver(user_logged_in)

View File

@ -0,0 +1,45 @@
"""Reputation tasks"""
from django.core.cache import cache
from structlog import get_logger
from passbook.core.models import User
from passbook.policies.reputation.models import IPReputation, UserReputation
from passbook.policies.reputation.signals import (
CACHE_KEY_IP_PREFIX,
CACHE_KEY_USER_PREFIX,
)
from passbook.root.celery import CELERY_APP
LOGGER = get_logger()
@CELERY_APP.task()
def save_ip_reputation():
"""Save currently cached reputation to database"""
keys = cache.keys(CACHE_KEY_IP_PREFIX + "*")
objects_to_update = []
for key in keys:
score = cache.get(key)
remote_ip = key.replace(CACHE_KEY_IP_PREFIX, "")
rep, _ = IPReputation.objects.get_or_create(ip=remote_ip)
rep.score = score
objects_to_update.append(rep)
IPReputation.objects.bulk_update(objects_to_update, ["score"])
@CELERY_APP.task()
def save_user_reputation():
"""Save currently cached reputation to database"""
keys = cache.keys(CACHE_KEY_USER_PREFIX + "*")
objects_to_update = []
for key in keys:
score = cache.get(key)
username = key.replace(CACHE_KEY_USER_PREFIX, "")
users = User.objects.filter(username=username)
if not users.exists():
LOGGER.info("User in cache does not exist, ignoring", username=username)
continue
rep, _ = UserReputation.objects.get_or_create(user=users.first())
rep.score = score
objects_to_update.append(rep)
UserReputation.objects.bulk_update(objects_to_update, ["score"])

View File

@ -0,0 +1,55 @@
"""test reputation signals and policy"""
from django.contrib.auth import authenticate
from django.core.cache import cache
from django.test import TestCase
from passbook.core.models import User
from passbook.policies.reputation.models import (
CACHE_KEY_IP_PREFIX,
CACHE_KEY_USER_PREFIX,
IPReputation,
ReputationPolicy,
UserReputation,
)
from passbook.policies.reputation.tasks import save_ip_reputation, save_user_reputation
from passbook.policies.types import PolicyRequest
class TestReputationPolicy(TestCase):
"""test reputation signals and policy"""
def setUp(self):
self.test_ip = "255.255.255.255"
self.test_username = "test"
cache.delete(CACHE_KEY_IP_PREFIX + self.test_ip)
cache.delete(CACHE_KEY_USER_PREFIX + self.test_username)
# We need a user for the one-to-one in userreputation
self.user = User.objects.create(username=self.test_username)
def test_ip_reputation(self):
"""test IP reputation"""
# Trigger negative reputation
authenticate(None, username=self.test_username, password=self.test_username)
# Test value in cache
self.assertEqual(cache.get(CACHE_KEY_IP_PREFIX + self.test_ip), -1)
# Save cache and check db values
save_ip_reputation()
self.assertEqual(IPReputation.objects.get(ip=self.test_ip).score, -1)
def test_user_reputation(self):
"""test User reputation"""
# Trigger negative reputation
authenticate(None, username=self.test_username, password=self.test_username)
# Test value in cache
self.assertEqual(cache.get(CACHE_KEY_USER_PREFIX + self.test_username), -1)
# Save cache and check db values
save_user_reputation()
self.assertEqual(UserReputation.objects.get(user=self.user).score, -1)
def test_policy(self):
"""Test Policy"""
request = PolicyRequest(user=self.user)
policy: ReputationPolicy = ReputationPolicy.objects.create(
name="reputation-test", threshold=0
)
self.assertTrue(policy.passes(request).passing)

View File

@ -14,7 +14,6 @@ def invalidate_policy_cache(sender, instance, **_):
from passbook.policies.models import Policy, PolicyBinding
if isinstance(instance, Policy):
LOGGER.debug("Invalidating policy cache", policy=instance)
total = 0
for binding in PolicyBinding.objects.filter(policy=instance):
prefix = (
@ -23,4 +22,4 @@ def invalidate_policy_cache(sender, instance, **_):
keys = cache.keys(prefix)
total += len(keys)
cache.delete_many(keys)
LOGGER.debug("Deleted keys", len=total)
LOGGER.debug("Invalidating policy cache", policy=instance, keys=total)

View File

@ -9,7 +9,7 @@ from passbook.policies.expression.models import ExpressionPolicy
from passbook.policies.models import Policy, PolicyBinding, PolicyBindingModel
class PolicyTestEngine(TestCase):
class TestPolicyEngine(TestCase):
"""PolicyEngine tests"""
def setUp(self):

View File

@ -0,0 +1,11 @@
"""Policy Utils"""
from typing import Any, Dict
def delete_none_keys(dict_: Dict[Any, Any]) -> Dict[Any, Any]:
"""Remove any keys from `dict_` that are None."""
new_dict = {}
for key, value in dict_.items():
if value is not None:
new_dict[key] = value
return new_dict

View File

@ -15,17 +15,17 @@ class ApplicationGatewayProviderForm(forms.ModelForm):
self.instance.client = Client.objects.create(
client_id=generate_client_id(), client_secret=generate_client_secret()
)
self.instance.client.reuse_consent = False # This is managed by passbook
self.instance.client.require_consent = False # This is managed by passbook
self.instance.client.name = self.instance.name
self.instance.client.response_types.set(
[ResponseType.objects.get_by_natural_key("code")]
)
self.instance.client.redirect_uris = [
f"http://{self.instance.external_host}/oauth2/callback",
f"https://{self.instance.external_host}/oauth2/callback",
f"http://{self.instance.internal_host}/oauth2/callback",
f"https://{self.instance.internal_host}/oauth2/callback",
f"{self.instance.external_host}/oauth2/callback",
f"{self.instance.internal_host}/oauth2/callback",
]
self.instance.client.scope = ["openid", "email"]
self.instance.client.scope = ["openid", "email", "profile"]
self.instance.client.save()
return super().save(*args, **kwargs)

View File

@ -0,0 +1,24 @@
# Generated by Django 3.0.8 on 2020-07-26 17:45
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_providers_app_gw", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="applicationgatewayprovider",
name="external_host",
field=models.TextField(validators=[django.core.validators.URLValidator]),
),
migrations.AlterField(
model_name="applicationgatewayprovider",
name="internal_host",
field=models.TextField(validators=[django.core.validators.URLValidator]),
),
]

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