Compare commits
564 Commits
version/0.
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
8a105cf5a0 | |||
9e384df79e | |||
c0bfd32d39 | |||
7be680cbe5 | |||
93bf8eaa82 | |||
1248585dca | |||
1319c480c4 | |||
1911e8e3a9 | |||
4198c5363f | |||
207aae15a8 | |||
50531b8a36 | |||
e5e4824920 | |||
085247e2dc | |||
f766594ab0 | |||
d1e469e282 | |||
79e4500827 | |||
42702fa96a | |||
9deb3ad80f | |||
9877ef99c4 | |||
c304b40e1b | |||
f0e6d6f417 | |||
54de5c981e | |||
a446775fe2 | |||
7393d8720b | |||
287cb72d6f | |||
c5eff4bdd6 | |||
e9a33ed8ab | |||
875173a86e | |||
df7642b365 | |||
3bc1c0aa8b | |||
8951f5695e | |||
7401278707 | |||
e99f6e289b | |||
07da6ffa69 | |||
dc18730094 | |||
a202679bfb | |||
1edcda58ba | |||
5cb7f0794e | |||
7e8e3893eb | |||
e91e286ebc | |||
ef4a115b61 | |||
b79b73f5c6 | |||
056e3ed15b | |||
fb5e210af8 | |||
e5e2615f15 | |||
6c72a9e2e8 | |||
c04d0a373a | |||
bd74e518a7 | |||
3b76af4eaa | |||
706448dc14 | |||
34793f7cef | |||
ba96c9526e | |||
617432deaa | |||
36bf2be16d | |||
912ed343e6 | |||
2e15df295a | |||
eaab3f62cb | |||
aa615b0fd6 | |||
b775f2788c | |||
9c28db3d89 | |||
67360bd6e9 | |||
4f6f8c7cae | |||
3b82ad798b | |||
8827f06ac1 | |||
251672a67d | |||
4ffc0e2a08 | |||
4e1808632d | |||
791627d3ce | |||
f3df3a0157 | |||
6aaae53a19 | |||
4d84f6d598 | |||
4e2349b6d9 | |||
cd57b8f7f3 | |||
40b1fc06b0 | |||
02fa217e28 | |||
6652514358 | |||
dcd3dc9744 | |||
d6afdc575e | |||
287b38efee | |||
e805fb62fb | |||
c92dda77f1 | |||
f12fd78822 | |||
caba183c9b | |||
3aeaa121a3 | |||
a9f3118a7d | |||
054b819262 | |||
6b3411f63b | |||
6a8000ea0d | |||
352d4db0d7 | |||
4b665cfb8f | |||
4e12003944 | |||
6bfd465855 | |||
e8670aa693 | |||
5263e750b1 | |||
a2a9d73296 | |||
6befc9d627 | |||
73497a27cc | |||
f3098418f2 | |||
a5197963b2 | |||
e4634bcc78 | |||
74da44a6a9 | |||
3324473cd0 | |||
39d8038533 | |||
bbcf58705f | |||
7b5a0964b2 | |||
8eca76e464 | |||
fb9ab368f8 | |||
877279b2ee | |||
301be4b411 | |||
728f527ccb | |||
3f1c790b1d | |||
b00573bde2 | |||
aeee3ad7f9 | |||
ef021495ef | |||
061eab4b36 | |||
870e01f836 | |||
e2ca72adf0 | |||
395ef43eae | |||
a4cc653757 | |||
db4ff20906 | |||
1f0fbd33b6 | |||
5de8d2721e | |||
0d65da9a9e | |||
4316ee4330 | |||
2ed9a1dbe3 | |||
8e03824d20 | |||
754dbdd0e5 | |||
e13d348315 | |||
169f3ebe5b | |||
f8ad604e85 | |||
774b9c8a61 | |||
d8c522233e | |||
82d50f7eaa | |||
1c426c5136 | |||
d6e14cc551 | |||
c3917ebc2e | |||
7203bd37a3 | |||
597188c7ee | |||
ac4c314042 | |||
05866d3544 | |||
6596bc6034 | |||
c6661ef4d2 | |||
386e23dfac | |||
5d7220ca70 | |||
5de0d03acf | |||
b0cc91f343 | |||
029a78f108 | |||
3f4a8dc4f6 | |||
32f6ba6302 | |||
8da0b14f29 | |||
83eb4aff02 | |||
927d02f591 | |||
d04afcd6d0 | |||
89c6db66fd | |||
e6ffa65a7e | |||
8a2f982a77 | |||
16cf6315e3 | |||
1d85874f41 | |||
ff64182ae8 | |||
a9ee67bf2d | |||
e87d52a76b | |||
8b09cf55a2 | |||
0203d20759 | |||
7861e2e0bd | |||
ad29d54bbf | |||
c698ba37d9 | |||
6a53069653 | |||
152b2d863d | |||
ee670d5e19 | |||
36e095671c | |||
1088b947a8 | |||
c4a30c50ac | |||
2831df45a0 | |||
ee5bac099f | |||
69f7b41044 | |||
f9cede7b31 | |||
903cdeaa7f | |||
e909e7fa8a | |||
bee38551f3 | |||
c0ec6388df | |||
8f08836885 | |||
dd0d7e7481 | |||
25d0ac6534 | |||
971713d1aa | |||
5135d828b4 | |||
b2c571bf1b | |||
6b1d30d230 | |||
3454760731 | |||
96846220c3 | |||
a4f5678144 | |||
a18baa3cb3 | |||
dfedd4a7f1 | |||
897f64600a | |||
c6eb015d18 | |||
54088239ab | |||
aa9c7a6567 | |||
6c0c12c90a | |||
c49b57ad1d | |||
2339e855bb | |||
bdc019c7cf | |||
5e2fb6d56e | |||
3b9524cdfc | |||
7154f19668 | |||
8fedd9ec07 | |||
4ac87d8739 | |||
e4f45eba0a | |||
4b3e0f0f96 | |||
482da81522 | |||
c5226fd0e8 | |||
7806cff96f | |||
fa504e4bf9 | |||
86cfb10b9b | |||
f6b8171624 | |||
91ce7f7363 | |||
17060238f0 | |||
c392c2a74b | |||
8cbaec8ba8 | |||
4750f8c653 | |||
69d2a1cf3b | |||
635f6c1ef2 | |||
18da7565c2 | |||
45699a1a69 | |||
5556e9f8e7 | |||
327bb09dd4 | |||
8ca23451c6 | |||
b99e2b10fe | |||
e966dff1a7 | |||
481fbedef2 | |||
d104012eee | |||
b03a508475 | |||
8ede4b6a13 | |||
41323afccc | |||
4a10b4999b | |||
20ee634cda | |||
713025d218 | |||
58ae159835 | |||
c95efe3cde | |||
b6eb0bf53d | |||
610b6c7f70 | |||
1ea2d99ff2 | |||
67be43679c | |||
fd42389bd5 | |||
71b1df2fec | |||
7a3122f25c | |||
63041d788b | |||
bfc1bae0bb | |||
8ab7f7fcbb | |||
c1eb8317f7 | |||
7a578e5e83 | |||
b10912d8ba | |||
ef24b1cde2 | |||
26cacc2a06 | |||
ca0e89c799 | |||
17950119ad | |||
876618c1ec | |||
2293ab69b9 | |||
9df00e09a4 | |||
cf6ce9c915 | |||
3b61191614 | |||
9954eeac86 | |||
ac88bd5d44 | |||
2406a619df | |||
63087c9393 | |||
da9aaf69df | |||
ae125dd1f0 | |||
f636595230 | |||
d506e8f1a3 | |||
d3a96ac7aa | |||
189b0ec324 | |||
c5a6b4961f | |||
b590589324 | |||
9fb1ac98ec | |||
195d8fe71f | |||
b0602a3215 | |||
0150a5c58c | |||
b35d27c83e | |||
801bb90806 | |||
55a83abb26 | |||
c09b4e9713 | |||
247015e955 | |||
fe3634be64 | |||
ead20b03aa | |||
932a475af7 | |||
e9a1a18ba3 | |||
6cd9edd38a | |||
9b5f9167cd | |||
1f30bcd335 | |||
94eaeb5a60 | |||
a5420fe019 | |||
2e1849a732 | |||
4039e96803 | |||
8f585eca70 | |||
516455f482 | |||
719099a5af | |||
7f74d32253 | |||
525d271535 | |||
9ef39f1e04 | |||
9099dc5713 | |||
c3c525a3f0 | |||
e699dfe88c | |||
c0b334eb02 | |||
815ad26b91 | |||
03647fa6af | |||
5aec581585 | |||
68e9b7e140 | |||
b42bca4e3e | |||
42c9ac61b2 | |||
7cdc5f0568 | |||
a063613f4c | |||
3af04bf1e4 | |||
74f8b68af8 | |||
59dbc15be7 | |||
9d5dd896f3 | |||
02f5f12089 | |||
90ea6dba90 | |||
b0b2c0830b | |||
acb2b825f3 | |||
e956b86649 | |||
739c66da1c | |||
e8c7cce68f | |||
f741d382c2 | |||
a13d4047b6 | |||
e0d8189442 | |||
760352202e | |||
9724ded194 | |||
5da4ff4ff1 | |||
e54b98a80e | |||
67b69cb5d3 | |||
863111ac57 | |||
bd78087582 | |||
8f4e954160 | |||
553f184aad | |||
b6d7847eae | |||
ad0d339794 | |||
737cd22bb9 | |||
6ad1465f8f | |||
d74fa4abbf | |||
b24938fc6b | |||
ea1564548c | |||
3663c3c8a1 | |||
07e20a2950 | |||
6366d50a0e | |||
c3e64df95b | |||
d2bf2c8896 | |||
f27b43507c | |||
c1058c7438 | |||
c37901feb9 | |||
44b815efae | |||
64a71a3663 | |||
ae435f423e | |||
7aa89c6d4f | |||
7e9d7e5198 | |||
2be6cd70d9 | |||
2b9705b33c | |||
502e43085f | |||
40f1de3b11 | |||
899c5b63ea | |||
e104c74761 | |||
5d46c1ea5a | |||
7d533889bc | |||
d9c2b32cba | |||
6e4ce8dbaa | |||
03d58b439f | |||
ea38da441b | |||
bdaf0111c2 | |||
974c2ddb11 | |||
769ce1c642 | |||
f294791d41 | |||
4ee22f8ec1 | |||
74d3cfbba0 | |||
d278acb83b | |||
84da454612 | |||
52101007aa | |||
dc57f433fd | |||
3d4c5b8f4e | |||
e66424cc49 | |||
8fa83a8d08 | |||
397892b282 | |||
7be50c2574 | |||
2aad523596 | |||
6982b97eb0 | |||
3de879496d | |||
4e75118a43 | |||
52c4fb431f | |||
d696d854ff | |||
6966c119a7 | |||
8cf5e647e3 | |||
99bc6241f6 | |||
e5f837ebb7 | |||
9d93da3d45 | |||
9f6f18f9bb | |||
6458b1dbf8 | |||
1aff9afca6 | |||
e0bc7d3932 | |||
9fd9b2611c | |||
6f3a1dfd08 | |||
464b2cce88 | |||
4eaa46e717 | |||
59e8dca499 | |||
945d5bfaf6 | |||
dbcdab05ff | |||
e2cc2843d8 | |||
241d59be8d | |||
74251a8883 | |||
585afd1bcd | |||
8358574484 | |||
cbcdaaf532 | |||
f99eaa85ac | |||
5007a6befe | |||
50c75087b8 | |||
438e4efd49 | |||
c7ca95ff2b | |||
9f403a71ed | |||
2f4139df65 | |||
f3ee8f7d9c | |||
5fa3729702 | |||
87f44fada4 | |||
c0026f3e16 | |||
c1051059f4 | |||
c25eda63ba | |||
c90906c968 | |||
f6b52b9281 | |||
b04f92c8b4 | |||
a02fcb0a7a | |||
c1ea605c7e | |||
116be0b3c0 | |||
438250b3a9 | |||
5e6acee2a5 | |||
8b4222e7bb | |||
4af563ce89 | |||
77842fab58 | |||
5689f25c39 | |||
a69c494feb | |||
83408b6ae0 | |||
d30abc64d0 | |||
6674d3e017 | |||
4749c3fad0 | |||
18886697d6 | |||
e75c9e9a79 | |||
5a3c1137ab | |||
ddca46e24a | |||
22a9abf7bf | |||
fb16502466 | |||
421bd13ddf | |||
404c9ef753 | |||
a57b545093 | |||
d8530f238d | |||
fe4a0c3b44 | |||
e0c104ee5c | |||
6ab8794754 | |||
316e6cb17f | |||
9d5d99290c | |||
20ffe833de | |||
d4d026bf6a | |||
dfe093b2b9 | |||
60739e620e | |||
d6cc6770b8 | |||
ddc1022461 | |||
2c2226610e | |||
cba78b4de7 | |||
1eeb64ee39 | |||
22dea62084 | |||
5ff1dd8426 | |||
da15a8878f | |||
bf33828ac1 | |||
950a1fc77e | |||
895e7d7393 | |||
3beca0574d | |||
990f5f0a43 | |||
97ce143efe | |||
cbbe174fd8 | |||
da3c640343 | |||
4b39c71de0 | |||
818f417fd8 | |||
f1ccef7f6a | |||
6187436518 | |||
9559ee7cb9 | |||
72e9c4e6fa | |||
97b8a025b3 | |||
ea9687c30b | |||
0a5e14a352 | |||
0325847c22 | |||
491dcc1159 | |||
6292049c74 | |||
1e97af772f | |||
5c622cd4d2 | |||
c4de808c4e | |||
8c604d225b | |||
c7daadfb18 | |||
683968c96e | |||
c94added99 | |||
61c00e5b39 | |||
566ebae065 | |||
9b62a6403b | |||
8c465b2026 | |||
6b7da71aa8 | |||
e95bbfab9a | |||
e401575894 | |||
6428801270 | |||
3e13c13619 | |||
92f79eb30e | |||
e7472de4bf | |||
494950ac65 | |||
4d51295db2 | |||
3bbded3555 | |||
b3262e2a82 | |||
40614a65fc | |||
3cf558d594 | |||
812cc0d2f1 | |||
e21ed92848 | |||
5184c4b7ef | |||
2c07859b68 | |||
ae6304c05e | |||
501683e3cb | |||
cc8afa8706 | |||
17a9e02bc0 | |||
6a669992a8 | |||
7ea5c22b6c | |||
b11d6a5891 | |||
49830367a7 | |||
e69ca5a229 | |||
a57d21f5e8 | |||
c7026407c6 | |||
69eecd6b60 | |||
810f10edfe | |||
1c57128f11 | |||
82eade3eb1 | |||
56a9dcc88d | |||
fe70d80189 | |||
e97e22c58a | |||
bb4e39aab6 | |||
a8744f443c | |||
7fe9b8f0b4 | |||
696aa7e5f6 | |||
e1d82aee1d | |||
151374f565 | |||
bebeff9f7f | |||
8b99afa34d | |||
b317852e8a | |||
24ae35c35a | |||
8e6bb48227 | |||
7a4e8af1ae | |||
0161205c82 | |||
ca0ba85023 | |||
c2ebaa7f64 | |||
23cccebb96 | |||
3f5d30e6fe | |||
ca735349f9 | |||
25ce8c6dc7 | |||
081ac0bcdb | |||
8a07b349ee | |||
b3468bc265 | |||
4edfad869f | |||
404f5d7912 | |||
8bea99a953 | |||
0b0ba33dce | |||
e3627b2cd9 | |||
37fac3ae00 | |||
17a90adf3e | |||
7c3590f8ef | |||
7471415e7f | |||
9339d496f9 | |||
e72000eb06 | |||
ec5ff7c14d |
@ -1,10 +1,10 @@
|
||||
[bumpversion]
|
||||
current_version = 0.10.0-rc1
|
||||
current_version = 0.12.11-stable
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
serialize = {major}.{minor}.{patch}-{release}
|
||||
message = new release: {new_version}
|
||||
message = release: {new_version}
|
||||
tag_name = version/{new_version}
|
||||
|
||||
[bumpversion:part:release]
|
||||
@ -15,9 +15,9 @@ values =
|
||||
beta
|
||||
stable
|
||||
|
||||
[bumpversion:file:README.md]
|
||||
[bumpversion:file:website/docs/installation/docker-compose.md]
|
||||
|
||||
[bumpversion:file:docs/installation/docker-compose.md]
|
||||
[bumpversion:file:website/docs/installation/kubernetes.md]
|
||||
|
||||
[bumpversion:file:docker-compose.yml]
|
||||
|
||||
@ -28,3 +28,5 @@ values =
|
||||
[bumpversion:file:.github/workflows/release.yml]
|
||||
|
||||
[bumpversion:file:passbook/__init__.py]
|
||||
|
||||
[bumpversion:file:proxy/pkg/version.go]
|
||||
|
@ -1,11 +1,12 @@
|
||||
[run]
|
||||
source = passbook
|
||||
relative_files = true
|
||||
omit =
|
||||
*/asgi.py
|
||||
manage.py
|
||||
*/migrations/*
|
||||
*/apps.py
|
||||
docs/
|
||||
website/
|
||||
|
||||
[report]
|
||||
sort = Cover
|
||||
|
@ -3,4 +3,4 @@ helm
|
||||
passbook-ui
|
||||
static
|
||||
*.env.yml
|
||||
node_modules/
|
||||
**/node_modules
|
||||
|
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Logs**
|
||||
Output of docker-compose logs or kubectl logs respectively
|
||||
|
||||
**Version and Deployment (please complete the following information):**
|
||||
- passbook version: [e.g. 0.10.0-stable]
|
||||
- Deployment: [e.g. docker-compose, helm]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
42
.github/dependabot.yml
vendored
Normal file
42
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: "/proxy"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
||||
- package-ecosystem: npm
|
||||
directory: "/passbook/static/static"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
||||
- package-ecosystem: docker
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
||||
- package-ecosystem: docker
|
||||
directory: "/proxy"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
54
.github/workflows/codeql-analysis.yml
vendored
54
.github/workflows/codeql-analysis.yml
vendored
@ -1,54 +0,0 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, admin-more-info, ci-deploy-dev, gh-pages, provider-saml-v2]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 20 * * 2'
|
||||
|
||||
jobs:
|
||||
analyse:
|
||||
name: Analyse
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
34
.github/workflows/release.yml
vendored
34
.github/workflows/release.yml
vendored
@ -1,6 +1,8 @@
|
||||
name: passbook-release
|
||||
name: passbook-on-release
|
||||
|
||||
on:
|
||||
release
|
||||
release:
|
||||
types: [published, created]
|
||||
|
||||
jobs:
|
||||
# Build
|
||||
@ -16,17 +18,26 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:0.10.0-rc1
|
||||
-t beryju/passbook:0.12.11-stable
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.10.0-rc1
|
||||
run: docker push beryju/passbook:0.12.11-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook:latest
|
||||
build-proxy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "^1.15"
|
||||
- name: prepare go api client
|
||||
run: |
|
||||
cd proxy
|
||||
go get -u github.com/go-swagger/go-swagger/cmd/swagger
|
||||
swagger generate client -f ../swagger.yaml -A passbook -t pkg/
|
||||
go build -v .
|
||||
- name: Docker Login Registry
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@ -37,11 +48,11 @@ jobs:
|
||||
cd proxy
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/passbook-proxy:0.10.0-rc1 \
|
||||
-t beryju/passbook-proxy:0.12.11-stable \
|
||||
-t beryju/passbook-proxy:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-proxy:0.10.0-rc1
|
||||
run: docker push beryju/passbook-proxy:0.12.11-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-proxy:latest
|
||||
build-static:
|
||||
@ -66,11 +77,11 @@ jobs:
|
||||
run: docker build
|
||||
--no-cache
|
||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||
-t beryju/passbook-static:0.10.0-rc1
|
||||
-t beryju/passbook-static:0.12.11-stable
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.10.0-rc1
|
||||
run: docker push beryju/passbook-static:0.12.11-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-static:latest
|
||||
test-release:
|
||||
@ -82,10 +93,13 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run test suite in final docker images
|
||||
run: |
|
||||
sudo apt-get install -y pwgen
|
||||
echo "PG_PASS=$(pwgen 40 1)" >> .env
|
||||
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
||||
docker-compose pull -q
|
||||
docker-compose up --no-start
|
||||
docker-compose start postgresql redis
|
||||
docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"
|
||||
docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test passbook"
|
||||
sentry-release:
|
||||
needs:
|
||||
- test-release
|
||||
@ -100,5 +114,5 @@ jobs:
|
||||
SENTRY_PROJECT: passbook
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
tagName: 0.10.0-rc1
|
||||
tagName: 0.12.11-stable
|
||||
environment: beryjuorg-prod
|
||||
|
14
.github/workflows/tag.yml
vendored
14
.github/workflows/tag.yml
vendored
@ -1,10 +1,10 @@
|
||||
name: passbook-on-tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'version/*'
|
||||
|
||||
name: passbook-version-tag
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Create Release from Tag
|
||||
@ -13,6 +13,10 @@ jobs:
|
||||
- uses: actions/checkout@master
|
||||
- name: Pre-release test
|
||||
run: |
|
||||
sudo apt-get install -y pwgen
|
||||
echo "PASSBOOK_TAG=latest" >> .env
|
||||
echo "PG_PASS=$(pwgen 40 1)" >> .env
|
||||
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
||||
docker-compose pull -q
|
||||
docker build \
|
||||
--no-cache \
|
||||
@ -20,7 +24,7 @@ jobs:
|
||||
-f Dockerfile .
|
||||
docker-compose up --no-start
|
||||
docker-compose start postgresql redis
|
||||
docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"
|
||||
docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test passbook"
|
||||
- name: Install Helm
|
||||
run: |
|
||||
apt update && apt install -y curl
|
||||
@ -30,7 +34,7 @@ jobs:
|
||||
helm dependency update helm/
|
||||
helm package helm/
|
||||
mv passbook-*.tgz passbook-chart.tgz
|
||||
- name: Extract verison number
|
||||
- name: Extract version number
|
||||
id: get_version
|
||||
uses: actions/github-script@0.2.0
|
||||
with:
|
||||
@ -45,7 +49,7 @@ jobs:
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ steps.get_version.outputs.result }}
|
||||
draft: false
|
||||
draft: true
|
||||
prerelease: false
|
||||
- name: Upload packaged Helm Chart
|
||||
id: upload-release-asset
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -199,3 +199,4 @@ local.env.yml
|
||||
|
||||
# Selenium Screenshots
|
||||
selenium_screenshots/**
|
||||
backups/
|
||||
|
@ -9,3 +9,4 @@ ignore-paths:
|
||||
|
||||
uses:
|
||||
- django
|
||||
- celery
|
||||
|
26
.pylintrc
26
.pylintrc
@ -1,9 +1,29 @@
|
||||
[MASTER]
|
||||
|
||||
disable=arguments-differ,no-self-use,fixme,locally-disabled,too-many-ancestors,too-few-public-methods,import-outside-toplevel,bad-continuation,signature-differs,similarities,cyclic-import
|
||||
disable =
|
||||
arguments-differ,
|
||||
no-self-use,
|
||||
fixme,
|
||||
locally-disabled,
|
||||
too-many-ancestors,
|
||||
too-few-public-methods,
|
||||
import-outside-toplevel,
|
||||
bad-continuation,
|
||||
signature-differs,
|
||||
similarities,
|
||||
cyclic-import,
|
||||
protected-access,
|
||||
unsubscriptable-object # remove when pylint is upgraded to 2.6
|
||||
|
||||
load-plugins=pylint_django,pylint.extensions.bad_builtin
|
||||
extension-pkg-whitelist=lxml
|
||||
|
||||
extension-pkg-whitelist=lxml,xmlsec
|
||||
|
||||
# Allow constants to be shorter than normal (and lowercase, for settings.py)
|
||||
const-rgx=[a-zA-Z0-9_]{1,40}$
|
||||
|
||||
ignored-modules=django-otp
|
||||
jobs=12
|
||||
generated-members=xmlsec.constants.*,xmlsec.tree.*,xmlsec.template.*
|
||||
ignore=migrations
|
||||
max-attributes=12
|
||||
max-branches=20
|
||||
|
28
Dockerfile
28
Dockerfile
@ -1,4 +1,4 @@
|
||||
FROM python:3.8-slim-buster as locker
|
||||
FROM python:3.9-slim-buster as locker
|
||||
|
||||
COPY ./Pipfile /app/
|
||||
COPY ./Pipfile.lock /app/
|
||||
@ -9,24 +9,38 @@ RUN pip install pipenv && \
|
||||
pipenv lock -r > requirements.txt && \
|
||||
pipenv lock -rd > requirements-dev.txt
|
||||
|
||||
FROM python:3.8-slim-buster
|
||||
FROM python:3.9-slim-buster
|
||||
|
||||
WORKDIR /
|
||||
COPY --from=locker /app/requirements.txt /
|
||||
COPY --from=locker /app/requirements-dev.txt /
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends postgresql-client-11 build-essential && \
|
||||
rm -rf /var/lib/apt/ && \
|
||||
pip install -r /requirements.txt --no-cache-dir && \
|
||||
apt-get install -y --no-install-recommends curl ca-certificates gnupg && \
|
||||
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
|
||||
echo "deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends postgresql-client-12 postgresql-client-11 build-essential libxmlsec1-dev pkg-config && \
|
||||
apt-get clean && \
|
||||
pip install -r /requirements.txt --no-cache-dir && \
|
||||
apt-get remove --purge -y build-essential && \
|
||||
apt-get autoremove --purge && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /passbook passbook
|
||||
apt-get autoremove --purge -y && \
|
||||
# This is quite hacky, but docker has no guaranteed Group ID
|
||||
# we could instead check for the GID of the socket and add the user dynamically,
|
||||
# but then we have to drop permmissions later
|
||||
groupadd -g 998 docker_998 && \
|
||||
groupadd -g 999 docker_999 && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /passbook passbook && \
|
||||
usermod -a -G docker_998 passbook && \
|
||||
usermod -a -G docker_999 passbook && \
|
||||
mkdir /backups && \
|
||||
chown passbook:passbook /backups
|
||||
|
||||
COPY ./passbook/ /passbook
|
||||
COPY ./manage.py /
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
|
||||
USER passbook
|
||||
STOPSIGNAL SIGINT
|
||||
|
||||
ENTRYPOINT [ "/lifecycle/bootstrap.sh" ]
|
||||
|
16
Makefile
16
Makefile
@ -1,20 +1,26 @@
|
||||
all: lint-fix lint coverage gen
|
||||
|
||||
coverage:
|
||||
coverage run --concurrency=multiprocessing manage.py test passbook --failfast
|
||||
coverage run --concurrency=multiprocessing manage.py test --failfast -v 3
|
||||
coverage combine
|
||||
coverage html
|
||||
coverage report
|
||||
|
||||
lint-fix:
|
||||
isort -rc .
|
||||
black .
|
||||
black passbook e2e lifecycle
|
||||
|
||||
lint:
|
||||
pyright
|
||||
bandit -r .
|
||||
pylint passbook
|
||||
pyright passbook e2e lifecycle
|
||||
bandit -r passbook e2e lifecycle -x node_modules
|
||||
pylint passbook e2e lifecycle
|
||||
prospector
|
||||
|
||||
gen: coverage
|
||||
./manage.py generate_swagger -o swagger.yaml -f yaml
|
||||
|
||||
local-stack:
|
||||
export PASSBOOK_TAG=testing
|
||||
docker build -t beryju/passbook:testng .
|
||||
docker-compose up -d
|
||||
docker-compose run --rm server migrate
|
||||
|
17
Pipfile
17
Pipfile
@ -17,10 +17,10 @@ django-otp = "*"
|
||||
django-prometheus = "*"
|
||||
django-recaptcha = "*"
|
||||
django-redis = "*"
|
||||
django-rest-framework = "*"
|
||||
djangorestframework = "*"
|
||||
django-storages = "*"
|
||||
djangorestframework-guardian = "*"
|
||||
drf-yasg = "*"
|
||||
drf_yasg2 = "*"
|
||||
facebook-sdk = "*"
|
||||
ldap3 = "*"
|
||||
lxml = "*"
|
||||
@ -28,14 +28,13 @@ packaging = "*"
|
||||
psycopg2-binary = "*"
|
||||
pycryptodome = "*"
|
||||
pyjwkest = "*"
|
||||
uvicorn = "*"
|
||||
uvicorn = {extras = ["standard"],version = "*"}
|
||||
gunicorn = "*"
|
||||
pyyaml = "*"
|
||||
qrcode = "*"
|
||||
requests-oauthlib = "*"
|
||||
sentry-sdk = "*"
|
||||
service_identity = "*"
|
||||
signxml = "*"
|
||||
structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
@ -43,21 +42,23 @@ dacite = "*"
|
||||
channels = "*"
|
||||
channels-redis = "*"
|
||||
kubernetes = "*"
|
||||
docker = "*"
|
||||
xmlsec = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
python_version = "3.9"
|
||||
|
||||
[dev-packages]
|
||||
autopep8 = "*"
|
||||
bandit = "*"
|
||||
black = "==19.10b0"
|
||||
black = "==20.8b1"
|
||||
bumpversion = "*"
|
||||
colorama = "*"
|
||||
coverage = "*"
|
||||
django-debug-toolbar = "*"
|
||||
docker = "*"
|
||||
pylint = "*"
|
||||
pylint-django = "*"
|
||||
selenium = "*"
|
||||
unittest-xml-reporting = "*"
|
||||
prospector = "*"
|
||||
pytest = "*"
|
||||
pytest-django = "*"
|
||||
|
1212
Pipfile.lock
generated
1212
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
25
README.md
25
README.md
@ -1,4 +1,4 @@
|
||||
<img src="passbook/static/static/passbook/logo.svg" height="50" alt="passbook logo"><img src="passbook/static/static/passbook/brand_inverted.svg" height="50" alt="passbook">
|
||||
<img src="website/static/img/logo.svg" height="50" alt="passbook logo"><img src="website/static/img/brand_inverted.svg" height="50" alt="passbook">
|
||||
|
||||
[](https://dev.azure.com/beryjuorg/passbook/_build?definitionId=1)
|
||||

|
||||
@ -13,31 +13,18 @@ passbook is an open-source Identity Provider focused on flexibility and versatil
|
||||
|
||||
## Installation
|
||||
|
||||
For small/test setups it is recommended to use docker-compose.
|
||||
For small/test setups it is recommended to use docker-compose, see the [documentation](https://passbook.beryju.org/website/docs/installation/docker-compose/)
|
||||
|
||||
```
|
||||
wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
|
||||
# Optionally enable Error-reporting
|
||||
# export PASSBOOK_ERROR_REPORTING=true
|
||||
# Optionally deploy a different version
|
||||
# export PASSBOOK_TAG=0.10.0-rc1
|
||||
# If this is a productive installation, set a different PostgreSQL Password
|
||||
# export PG_PASS=$(pwgen 40 1)
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker-compose run --rm server migrate
|
||||
```
|
||||
|
||||
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://passbook.beryju.org//installation/kubernetes/)
|
||||
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://passbook.beryju.org/website/docs/installation/kubernetes/)
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Development
|
||||
|
||||
To develop on passbook, you need a system with Python 3.7+ (3.8 is recommended). passbook uses [pipenv](https://pipenv.pypa.io/en/latest/) for managing dependencies.
|
||||
To develop on passbook, you need a system with Python 3.8+ (3.9 is recommended). passbook uses [pipenv](https://pipenv.pypa.io/en/latest/) for managing dependencies.
|
||||
|
||||
To get started, run
|
||||
|
||||
|
@ -6,7 +6,9 @@ As passbook is currently in a pre-stable, only the latest "stable" version is su
|
||||
|
||||
| Version | Supported |
|
||||
| -------- | ------------------ |
|
||||
| 0.8.15 | :white_check_mark: |
|
||||
| 0.10.x | :white_check_mark: |
|
||||
| 0.11.x | :white_check_mark: |
|
||||
| 0.12.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
@ -8,6 +8,10 @@ variables:
|
||||
POSTGRES_DB: passbook
|
||||
POSTGRES_USER: passbook
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
${{ if startsWith(variables['Build.SourceBranch'], 'refs/heads/') }}:
|
||||
branchName: ${{ replace(variables['Build.SourceBranchName'], 'refs/heads/', '') }}
|
||||
${{ if startsWith(variables['Build.SourceBranch'], 'refs/pull/') }}:
|
||||
branchName: ${{ replace(variables['System.PullRequest.SourceBranch'], 'refs/heads/', '') }}
|
||||
|
||||
stages:
|
||||
- stage: Lint
|
||||
@ -18,61 +22,65 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run pylint passbook
|
||||
script: pipenv run pylint passbook e2e lifecycle
|
||||
- job: black
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run black --check passbook
|
||||
script: pipenv run black --check passbook e2e lifecycle
|
||||
- job: prospector
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
pipenv install --dev prospector --skip-lock
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run prospector passbook
|
||||
script: pipenv run prospector
|
||||
- job: bandit
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run bandit -r passbook
|
||||
script: pipenv run bandit -r passbook e2e lifecycle
|
||||
- job: pyright
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
@ -82,18 +90,19 @@ stages:
|
||||
version: '12.x'
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: npm install -g pyright
|
||||
script: npm install -g pyright@1.1.79
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run pyright
|
||||
script: pipenv run pyright e2e lifecycle
|
||||
- stage: Test
|
||||
jobs:
|
||||
- job: migrations
|
||||
@ -102,7 +111,7 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
@ -112,18 +121,58 @@ stages:
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run ./manage.py migrate
|
||||
- job: migrations_from_previous_release
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.9'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
dockerComposeFile: 'scripts/ci.docker-compose.yml'
|
||||
action: 'Run services'
|
||||
buildImages: false
|
||||
- task: CmdLine@2
|
||||
displayName: Prepare Last tagged release
|
||||
inputs:
|
||||
script: |
|
||||
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
displayName: Migrate to last tagged release
|
||||
inputs:
|
||||
script: pipenv run ./manage.py migrate
|
||||
- task: CmdLine@2
|
||||
displayName: Install current branch
|
||||
inputs:
|
||||
script: |
|
||||
set -x
|
||||
git checkout ${{ variables.branchName }}
|
||||
pipenv sync --dev
|
||||
- task: CmdLine@2
|
||||
displayName: Migrate to current branch
|
||||
inputs:
|
||||
script: pipenv run ./manage.py migrate
|
||||
- job: coverage_unittest
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
@ -131,15 +180,27 @@ stages:
|
||||
action: 'Run services'
|
||||
buildImages: false
|
||||
- task: CmdLine@2
|
||||
displayName: Install K3d and prepare
|
||||
inputs:
|
||||
script: |
|
||||
wget -q -O - https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash
|
||||
k3d cluster create
|
||||
k3d kubeconfig write -o ~/.kube/config --overwrite
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
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
|
||||
export PB_TEST_K8S=true
|
||||
pipenv run coverage run ./manage.py test passbook -v 3
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
mkdir output-unittest
|
||||
mv unittest.xml output-unittest/unittest.xml
|
||||
mv .coverage output-unittest/coverage
|
||||
@ -150,11 +211,11 @@ stages:
|
||||
publishLocation: 'pipeline'
|
||||
- job: coverage_e2e
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
name: coventry
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
@ -162,8 +223,16 @@ stages:
|
||||
action: 'Run services'
|
||||
buildImages: false
|
||||
- task: CmdLine@2
|
||||
displayName: Install K3d and prepare
|
||||
inputs:
|
||||
script: |
|
||||
wget -q -O - https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash
|
||||
k3d cluster create
|
||||
k3d kubeconfig write -o ~/.kube/config --overwrite
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: DockerCompose@0
|
||||
@ -177,11 +246,21 @@ stages:
|
||||
inputs:
|
||||
script: |
|
||||
cd passbook/static/static
|
||||
yarn
|
||||
npm i
|
||||
npm run build
|
||||
- task: CmdLine@2
|
||||
displayName: Run full test suite
|
||||
inputs:
|
||||
script: pipenv run coverage run ./manage.py test e2e
|
||||
script: |
|
||||
export PB_TEST_K8S=true
|
||||
pipenv run coverage run ./manage.py test e2e -v 3 --failfast
|
||||
- task: CmdLine@2
|
||||
condition: always()
|
||||
displayName: Cleanup
|
||||
inputs:
|
||||
script: |
|
||||
docker stop $(docker ps -aq)
|
||||
docker container prune -f
|
||||
- task: CmdLine@2
|
||||
displayName: Prepare unittests and coverage for upload
|
||||
inputs:
|
||||
@ -219,17 +298,16 @@ stages:
|
||||
path: "coverage-unittest/"
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
find .
|
||||
pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage
|
||||
pipenv run coverage xml
|
||||
pipenv run coverage html
|
||||
find .
|
||||
- task: PublishCodeCoverageResults@1
|
||||
inputs:
|
||||
codeCoverageTool: 'Cobertura'
|
||||
@ -260,7 +338,7 @@ stages:
|
||||
repository: 'beryju/passbook'
|
||||
command: 'buildAndPush'
|
||||
Dockerfile: 'Dockerfile'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
tags: "gh-${{ variables.branchName }}"
|
||||
- job: build_static
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
@ -277,27 +355,11 @@ stages:
|
||||
repository: 'beryju/passbook-static'
|
||||
command: 'build'
|
||||
Dockerfile: 'static.Dockerfile'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
tags: "gh-${{ variables.branchName }}"
|
||||
arguments: "--network=beryjupassbook_default"
|
||||
- task: Docker@2
|
||||
inputs:
|
||||
containerRegistry: 'dockerhub'
|
||||
repository: 'beryju/passbook-static'
|
||||
command: 'push'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
- stage: Deploy
|
||||
jobs:
|
||||
- job: deploy_dev
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: HelmDeploy@0
|
||||
inputs:
|
||||
connectionType: 'Kubernetes Service Connection'
|
||||
kubernetesServiceConnection: 'k8s-beryjuorg-prd'
|
||||
namespace: 'passbook-dev'
|
||||
command: 'upgrade'
|
||||
chartType: 'FilePath'
|
||||
chartPath: 'helm/'
|
||||
releaseName: 'passbook-dev'
|
||||
recreate: true
|
||||
tags: "gh-${{ variables.branchName }}"
|
||||
|
@ -3,7 +3,7 @@ version: '3.2'
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres
|
||||
image: postgres:12
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
networks:
|
||||
@ -12,55 +12,68 @@ services:
|
||||
- POSTGRES_PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
||||
- POSTGRES_USER=passbook
|
||||
- POSTGRES_DB=passbook
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
env_file:
|
||||
- .env
|
||||
redis:
|
||||
image: redis
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
server:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-rc1}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.11-stable}
|
||||
command: server
|
||||
environment:
|
||||
PASSBOOK_REDIS__HOST: redis
|
||||
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
|
||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword}
|
||||
PASSBOOK_LOG_LEVEL: debug
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
ports:
|
||||
- 8000
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
- traefik.port=8000
|
||||
- traefik.docker.network=internal
|
||||
- traefik.frontend.rule=PathPrefix:/
|
||||
traefik.enable: 'true'
|
||||
traefik.docker.network: internal
|
||||
traefik.http.routers.app-router.rule: PathPrefix(`/`)
|
||||
traefik.http.routers.app-router.service: app-service
|
||||
traefik.http.routers.app-router.tls: 'true'
|
||||
traefik.http.services.app-service.loadbalancer.healthcheck.hostname: passbook-healthcheck-host
|
||||
traefik.http.services.app-service.loadbalancer.server.port: '8000'
|
||||
env_file:
|
||||
- .env
|
||||
worker:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-rc1}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.11-stable}
|
||||
command: worker
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
environment:
|
||||
PASSBOOK_REDIS__HOST: redis
|
||||
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
|
||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword}
|
||||
PASSBOOK_LOG_LEVEL: debug
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
volumes:
|
||||
- ./backups:/backups
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
env_file:
|
||||
- .env
|
||||
static:
|
||||
image: beryju/passbook-static:0.10.0-rc1
|
||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.11-stable}
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
- traefik.frontend.rule=PathPrefix:/static, /robots.txt, /favicon.ico
|
||||
- traefik.port=80
|
||||
- traefik.docker.network=internal
|
||||
traefik.enable: 'true'
|
||||
traefik.docker.network: internal
|
||||
traefik.http.routers.static-router.rule: PathPrefix(`/static`, `/robots.txt`, `/favicon.ico`)
|
||||
traefik.http.routers.static-router.tls: 'true'
|
||||
traefik.http.routers.static-router.service: static-service
|
||||
traefik.http.services.static-service.loadbalancer.healthcheck.path: /
|
||||
traefik.http.services.static-service.loadbalancer.server.port: '80'
|
||||
traefik:
|
||||
image: traefik:1.7
|
||||
command: --api --docker --defaultentrypoints=https --entryPoints='Name:http Address::80 Redirect.EntryPoint:https' --entryPoints='Name:https Address::443 TLS'
|
||||
image: traefik:2.3
|
||||
command:
|
||||
- "--log.format=json"
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.http.address=:80"
|
||||
- "--entrypoints.https.address=:443"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
ports:
|
||||
|
@ -1,21 +0,0 @@
|
||||
# Passbook User Object
|
||||
|
||||
The User object has the following attributes:
|
||||
|
||||
- `username`: User's username.
|
||||
- `email` User's email.
|
||||
- `name` User's display mame.
|
||||
- `is_staff` Boolean field if user is staff.
|
||||
- `is_active` Boolean field if user is active.
|
||||
- `date_joined` Date user joined/was created.
|
||||
- `password_change_date` Date password was last changed.
|
||||
- `attributes` Dynamic attributes.
|
||||
|
||||
## Examples
|
||||
|
||||
List all the User's group names:
|
||||
|
||||
```python
|
||||
for group in user.groups.all():
|
||||
yield group.name
|
||||
```
|
@ -1,7 +0,0 @@
|
||||
# OTP Stage
|
||||
|
||||
This stage offers a generic Time-based One-time Password authentication step.
|
||||
|
||||
You can optionally enforce this step, which will force every user without OTP setup to configure it.
|
||||
|
||||
This stage uses a 6-digit Code with a 30 second time-drift. This is currently not changeable.
|
@ -1,8 +0,0 @@
|
||||
# User Delete Stage
|
||||
|
||||
!!! danger
|
||||
This stage deletes the `pending_user` without any confirmation. You have to make sure the user is aware of this.
|
||||
|
||||
This stage is intended for an unenrollment flow. It deletes the currently pending user.
|
||||
|
||||
The pending user is also removed from the current session.
|
Binary file not shown.
Before Width: | Height: | Size: 253 KiB |
Binary file not shown.
Before Width: | Height: | Size: 338 KiB |
@ -1,72 +0,0 @@
|
||||
# Kubernetes
|
||||
|
||||
For a mid to high-load installation, Kubernetes is recommended. passbook is installed using a helm-chart.
|
||||
|
||||
This installation automatically applies database migrations on startup. After the installation is done, you can use `pbadmin` as username and password.
|
||||
|
||||
```
|
||||
###################################
|
||||
# Values directly affecting passbook
|
||||
###################################
|
||||
image:
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.9.0-stable
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
serverReplicas: 1
|
||||
workerReplicas: 1
|
||||
|
||||
config:
|
||||
# Optionally specify fixed secret_key, otherwise generated automatically
|
||||
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# Enable error reporting
|
||||
error_reporting:
|
||||
enabled: false
|
||||
environment: customer
|
||||
send_pii: false
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
log_level: warning
|
||||
|
||||
# Enable Database Backups to S3
|
||||
# backup:
|
||||
# access_key: access-key
|
||||
# secret_key: secret-key
|
||||
# bucket: s3-bucket
|
||||
# host: s3-host
|
||||
|
||||
###################################
|
||||
# Values controlling dependencies
|
||||
###################################
|
||||
|
||||
install:
|
||||
postgresql: true
|
||||
redis: true
|
||||
|
||||
# These values influence the bundled postgresql and redis charts, but are also used by passbook to connect
|
||||
postgresql:
|
||||
postgresqlDatabase: passbook
|
||||
|
||||
redis:
|
||||
cluster:
|
||||
enabled: false
|
||||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
# https://stackoverflow.com/a/59189742
|
||||
disableCommands: []
|
||||
|
||||
ingress:
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
path: /
|
||||
hosts:
|
||||
- passbook.k8s.local
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - passbook.k8s.local
|
||||
```
|
@ -1,33 +0,0 @@
|
||||
# Amazon Web Services Integration
|
||||
|
||||
## What is AWS
|
||||
|
||||
!!! note ""
|
||||
Amazon Web Services (AWS) is the world’s most comprehensive and broadly adopted cloud platform, offering over 175 fully featured services from data centers globally. Millions of customers—including the fastest-growing startups, largest enterprises, and leading government agencies—are using AWS to lower costs, become more agile, and innovate faster.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Create an application in passbook and note the slug, as this will be used later. Create a SAML provider with the following parameters:
|
||||
|
||||
- ACS URL: `https://signin.aws.amazon.com/saml`
|
||||
- Audience: `urn:amazon:webservices`
|
||||
- Issuer: `passbook`
|
||||
- Binding: `Post`
|
||||
|
||||
You can of course use a custom signing certificate, and adjust durations.
|
||||
|
||||
## AWS
|
||||
|
||||
Create a role with the permissions you desire, and note the ARN.
|
||||
|
||||
AWS requires two custom PropertyMappings; `Role` and `RoleSessionName`. Create them as following:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Afterwards export the metadata from passbook, and create an Identity Provider [here](https://console.aws.amazon.com/iam/home#/providers).
|
Binary file not shown.
Before Width: | Height: | Size: 65 KiB |
Binary file not shown.
Before Width: | Height: | Size: 66 KiB |
@ -1,41 +0,0 @@
|
||||
# Sentry Integration
|
||||
|
||||
## What is Sentry
|
||||
|
||||
From https://sentry.io
|
||||
|
||||
!!! note ""
|
||||
Sentry provides self-hosted and cloud-based error monitoring that helps all software
|
||||
teams discover, triage, and prioritize errors in real-time.
|
||||
|
||||
One million developers at over fifty thousand companies already ship
|
||||
better software faster with Sentry. Won’t you join them?
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `sentry.company` is the FQDN of the Sentry install.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Create an application in passbook. Create an OpenID provider with the following parameters:
|
||||
|
||||
- Client Type: `Confidential`
|
||||
- Response types: `code (Authorization Code Flow)`
|
||||
- JWT Algorithm: `RS256`
|
||||
- Redirect URIs: `https://sentry.company/auth/sso/`
|
||||
- Scopes: `openid email`
|
||||
|
||||
## Sentry
|
||||
|
||||
**This guide assumes you've installed Sentry using [getsentry/onpremise](https://github.com/getsentry/onpremise)**
|
||||
|
||||
- Add `sentry-auth-oidc` to `onpremise/sentry/requirements.txt` (Create the file if it doesn't exist yet)
|
||||
- Add the following block to your `onpremise/sentry/sentry.conf.py`:
|
||||
```
|
||||
OIDC_ISSUER = "passbook"
|
||||
OIDC_CLIENT_ID = "<Client ID from passbook>"
|
||||
OIDC_CLIENT_SECRET = "<Client Secret from passbook>"
|
||||
OIDC_SCOPE = "openid email"
|
||||
OIDC_DOMAIN = "https://passbook.company/application/oidc/"
|
||||
```
|
@ -1,74 +0,0 @@
|
||||
# Ansible Tower / AWX Integration
|
||||
|
||||
## What is Tower
|
||||
|
||||
From https://docs.ansible.com/ansible/2.5/reference_appendices/tower.html
|
||||
|
||||
!!! note ""
|
||||
Ansible Tower (formerly ‘AWX’) is a web-based solution that makes Ansible even more easy to use for IT teams of all kinds. It’s designed to be the hub for all of your automation tasks.
|
||||
|
||||
Tower allows you to control access to who can access what, even allowing sharing of SSH credentials without someone being able to transfer those credentials. Inventory can be graphically managed or synced with a wide variety of cloud sources. It logs all of your jobs, integrates well with LDAP, and has an amazing browsable REST API. Command line tools are available for easy integration with Jenkins as well. Provisioning callbacks provide great support for autoscaling topologies.
|
||||
|
||||
!!! note
|
||||
AWX is the open-source version of Tower. The term "AWX" will be used interchangeably throughout this document.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `awx.company` is the FQDN of the AWX/Tower install.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Create an application in passbook and note the slug, as this will be used later. Create a SAML provider with the following parameters:
|
||||
|
||||
- ACS URL: `https://awx.company/sso/complete/saml/`
|
||||
- Audience: `awx`
|
||||
- Issuer: `https://awx.company/sso/metadata/saml/`
|
||||
|
||||
You can of course use a custom signing certificate, and adjust durations.
|
||||
|
||||
## AWX Configuration
|
||||
|
||||
Navigate to `https://awx.company/#/settings/auth` to configure SAML. Set the Field `SAML SERVICE PROVIDER ENTITY ID` to `awx`.
|
||||
|
||||
For the fields `SAML SERVICE PROVIDER PUBLIC CERTIFICATE` and `SAML SERVICE PROVIDER PRIVATE KEY`, you can either use custom certificates, or use the self-signed pair generated by passbook.
|
||||
|
||||
Provide metadata in the `SAML Service Provider Organization Info` field:
|
||||
|
||||
```json
|
||||
{
|
||||
"en-US": {
|
||||
"name": "passbook",
|
||||
"url": "https://passbook.company",
|
||||
"displayname": "passbook"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Provide metadata in the `SAML Service Provider Technical Contact` and `SAML Service Provider Technical Contact` fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"givenName": "Admin Name",
|
||||
"emailAddress": "admin@company"
|
||||
}
|
||||
```
|
||||
|
||||
In the `SAML Enabled Identity Providers` paste the following configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"passbook": {
|
||||
"attr_username": "urn:oid:2.16.840.1.113730.3.1.241",
|
||||
"attr_user_permanent_id": "urn:oid:0.9.2342.19200300.100.1.1",
|
||||
"x509cert": "MIIDEjCCAfqgAwIBAgIRAJZ9pOZ1g0xjiHtQAAejsMEwDQYJKoZIhvcNAQELBQAwMDEuMCwGA1UEAwwlcGFzc2Jvb2sgU2VsZi1zaWduZWQgU0FNTCBDZXJ0aWZpY2F0ZTAeFw0xOTEyMjYyMDEwNDFaFw0yMDEyMjYyMDEwNDFaMFkxLjAsBgNVBAMMJXBhc3Nib29rIFNlbGYtc2lnbmVkIFNBTUwgQ2VydGlmaWNhdGUxETAPBgNVBAoMCHBhc3Nib29rMRQwEgYDVQQLDAtTZWxmLXNpZ25lZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO/ktBYZkY9xAijF4acvzX6Q1K8KoIZeyde8fVgcWBz4L5FgDQ4/dni4k2YAcPdwteGL4nKVzetUzjbRCBUNuO6lqU4J4WNNX4Xg4Ir7XLRoAQeo+omTPBdpJ1p02HjtN5jT01umN3bK2yto1e37CJhK6WJiaXqRewPxh4lI4aqdj3BhFkJ3I3r2qxaWOAXQ6X7fg3w/ny7QP53//ouZo7hSLY3GIcRKgvdjjVM3OW5C3WLpOq5Dez5GWVJ17aeFCfGQ8bwFKde6qfYqyGcU9xHB36TtVHB9hSFP/tUFhkiSOxtsrYwCgCyXm4UTSpP+wiNyjKfFw7qGLBvA2hGTNw8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh9PeAqPRQk1/SSygIFADZBi08O/DPCshFwEHvJATIcTzcDD8UGAjXh+H5OlkDyX7KyrcaNvYaafCUo63A+WprdtdY5Ty6SBEwTYyiQyQfwM9BfK+imCoif1Ai7xAelD7p9lNazWq7JU+H/Ep7U7Q7LvpxAbK0JArt+IWTb2NcMb3OWE1r0gFbs44O1l6W9UbJTbyLMzbGbe5i+NHlgnwPwuhtRMh0NUYabGHKcHbhwyFhfGAQv2dAp5KF1E5gu6ZzCiFePzc0FrqXQyb2zpFYcJHXquiqaOeG7cZxRHYcjrl10Vxzki64XVA9BpdELgKSnupDGUEJsRUt3WVOmvZuA==",
|
||||
"url": "https://passbook.company/application/saml/awx/login/",
|
||||
"attr_last_name": "User.LastName",
|
||||
"entity_id": "https://awx.company/sso/metadata/saml/",
|
||||
"attr_email": "urn:oid:0.9.2342.19200300.100.1.3",
|
||||
"attr_first_name": "urn:oid:2.5.4.3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`x509cert` is the certificate configured in passbook. Remove the `--BEGIN CERTIFICATE--` and `--END CERTIFICATE--` headers, then enter the cert as one non-breaking string.
|
@ -1,26 +0,0 @@
|
||||
# Outposts
|
||||
|
||||
An outpost is a single deployment of a passbook component, which can be deployed in a completely separate environment. Currently, only the Proxy Provider is supported as outpost.
|
||||
|
||||

|
||||
|
||||
Upon creation, a service account and a token is generated. The service account only has permissions to read the outpost and provider configuration. This token is used by the Outpost to connect to passbook.
|
||||
|
||||
To deploy an outpost, you can for example use this docker-compose snippet:
|
||||
|
||||
```yaml
|
||||
version: 3.5
|
||||
|
||||
services:
|
||||
passbook_proxy:
|
||||
image: beryju/passbook-proxy:0.10.0-stable
|
||||
ports:
|
||||
- 4180:4180
|
||||
- 4443:4443
|
||||
environment:
|
||||
PASSBOOK_HOST: https://your-passbook.tld
|
||||
PASSBOOK_INSECURE: 'true'
|
||||
PASSBOOK_TOKEN: token-generated-by-passbook
|
||||
```
|
||||
|
||||
In future versions, this snippet will be automatically generated. You will also be able to deploy an outpost directly into a kubernetes cluster.w
|
@ -1,30 +0,0 @@
|
||||
# Expression Policies
|
||||
|
||||
!!! notice
|
||||
These variables are available in addition to the common variables/functions defined in [**Expressions**](../expressions/index.md)
|
||||
|
||||
The passing of the policy is determined by the return value of the code. Use `return True` to pass a policy and `return False` to fail it.
|
||||
|
||||
### Available Functions
|
||||
|
||||
#### `pb_message(message: str)`
|
||||
|
||||
Add a message, visible by the end user. This can be used to show the reason why they were denied.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
pb_message("Access denied")
|
||||
return False
|
||||
```
|
||||
|
||||
### Context variables
|
||||
|
||||
- `request`: A PolicyRequest object, which has the following properties:
|
||||
- `request.user`: The current user, against which the policy is applied. ([ref](../expressions/reference/user-object.md))
|
||||
- `request.http_request`: The Django HTTP Request. ([ref](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects))
|
||||
- `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. Can be [compared](../expressions/index.md#comparing-ip-addresses)
|
||||
- `pb_flow_plan`: Current Plan if Policy is called from the Flow Planner.
|
@ -1,12 +0,0 @@
|
||||
# Property Mapping Expressions
|
||||
|
||||
The property mapping should return a value that is expected by the Provider/Source. Supported types are documented in the individual Provider/Source. Returning `None` is always accepted and would simply skip the mapping for which `None` was returned.
|
||||
|
||||
!!! notice
|
||||
These variables are available in addition to the common variables/functions defined in [**Expressions**](../expressions/index.md)
|
||||
|
||||
### Context Variables
|
||||
|
||||
- `user`: The current user. This may be `None` if there is no contextual user. ([ref](../expressions/reference/user-object.md))
|
||||
- `request`: The current request. This may be `None` if there is no contextual request. ([ref](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects))
|
||||
- Other arbitrary arguments given by the provider, this is documented on the Provider/Source.
|
@ -1,31 +0,0 @@
|
||||
# OAuth2 Provider
|
||||
|
||||
This provider supports both generic OAuth2 as well as OpenID Connect
|
||||
|
||||
Scopes can be configured using Scope Mappings, a type of [Property Mappings](../property-mappings/index.md#scope-mapping).
|
||||
|
||||
Endpoint | URL
|
||||
---------|---
|
||||
Authorization | `/application/o/authorize/`
|
||||
Token | `/application/o/token/`
|
||||
User Info | `/application/o/userinfo/`
|
||||
End Session | `/application/o/end-session/`
|
||||
Introspect | `/application/o/end-session/`
|
||||
JWKS | `/application/o/<application slug>/jwks/`
|
||||
OpenID Configuration | `/application/o/<application slug>/.well-known/openid-configuration`
|
||||
|
||||
## GitHub Compatibility
|
||||
|
||||
This provider also exposes a GitHub-compatible endpoint. This endpoint can be used by applications, which support authenticating against GitHub Enterprise, but not generic OpenID Connect.
|
||||
|
||||
To use any of the GitHub Compatibility scopes, you have to use the GitHub Compatibility Endpoints.
|
||||
|
||||
|
||||
Endpoint | URL
|
||||
---------|---
|
||||
Authorization | `/login/oauth/authorize`
|
||||
Token | `/login/oauth/access_token`
|
||||
User Info | `/user`
|
||||
User Teams Info | `/user/teams`
|
||||
|
||||
To access the user's email address, a scope of `user:email` is required. To access their groups, `read:org` is required. Because these scopes are handled by a different endpoint, they are not customisable as a Scope Mapping.
|
@ -1,16 +0,0 @@
|
||||
# Proxy Provider
|
||||
|
||||
!!! info
|
||||
This provider is to be used in conjunction with [Outposts](../outposts/outposts.md)
|
||||
|
||||
This provider protects applications, which have no built-in support for OAuth2 or SAML. This is done by running a lightweight Reverse Proxy in front of the application, which authenticates the requests.
|
||||
|
||||
passbook Proxy is based on [oauth2_proxy](https://github.com/oauth2-proxy/oauth2-proxy), but has been integrated more tightly with passbook.
|
||||
|
||||
The Proxy these extra headers to the application:
|
||||
|
||||
Header Name | Value
|
||||
-------------|-------
|
||||
X-Auth-Request-User | The user's unique identifier
|
||||
X-Auth-Request-Email | The user's email address
|
||||
X-Auth-Request-Preferred-Username | The user's username
|
@ -1,12 +0,0 @@
|
||||
# SAML Provider
|
||||
|
||||
This provider allows you to integrate enterprise software using the SAML2 Protocol. It supports signed requests and uses [Property Mappings](../property-mappings/index.md#saml-property-mapping) to determine which fields are exposed and what values they return. This makes it possible to expose vendor-specific fields.
|
||||
Default fields are exposed through auto-generated Property Mappings, which are prefixed with "Autogenerated".
|
||||
|
||||
|
||||
Endpoint | URL
|
||||
---------|---
|
||||
SSO (Redirect binding) | `/application/saml/<application slug>/sso/binding/redirect/`
|
||||
SSO (POST binding) | `/application/saml/<application slug>/sso/binding/post/`
|
||||
IdP-initiated login | `/application/saml/<application slug>/sso/binding/init/`
|
||||
Metadata Download | `/application/saml/<application slug>/metadata/`
|
@ -1,2 +0,0 @@
|
||||
mkdocs
|
||||
mkdocs-material
|
@ -1 +0,0 @@
|
||||
3.7
|
@ -1,73 +0,0 @@
|
||||
# Upgrading to 0.10
|
||||
|
||||
This update brings a lot of big features, such as:
|
||||
|
||||
- New OAuth2/OpenID Provider
|
||||
|
||||
This new provider merges both OAuth2 and OpenID. It is based on the codebase of the old provider, which has been simplified and cleaned from the ground up. Support for Property Mappings has also been added. Because of this change, OpenID and OAuth2 Providers will have to be re-created.
|
||||
|
||||
- Proxy Provider
|
||||
|
||||
Due to this new OAuth2 Provider, the Application Gateway Provider, now simply called "Proxy Provider" has been revamped as well. The new passbook Proxy integrates more tightly with passbook via the new Outposts system. The new proxy also supports multiple applications per proxy instance, can configure TLS based on passbook Keypairs, and more.
|
||||
|
||||
See [Proxy](../providers/proxy.md)
|
||||
|
||||
- Outpost System
|
||||
|
||||
This is a new Object type, currently used only by the Proxy Provider. It manages the creation and permissions of service accounts, which are used by the outposts to communicate with passbook.
|
||||
|
||||
See [Outposts](../outposts/outposts.md)
|
||||
|
||||
- Flow Import/Export
|
||||
|
||||
Flows can now be imported and exported. This feature can be used as a backup system, or to share complex flows with other people. Example flows have also been added to the documentation to help you get going with passbook.
|
||||
|
||||
## Under the hood
|
||||
|
||||
- passbook now runs on Django 3.1 and Channels with complete ASGI enabled
|
||||
- uwsgi has been replaced with Gunicorn and uvicorn
|
||||
- Elastic APM has been replaced with Sentry Performance metrics
|
||||
- Flow title is now configurable separately from the name
|
||||
- All logging output is now json
|
||||
|
||||
## Upgrading
|
||||
|
||||
### docker-compose
|
||||
|
||||
The docker-compose file has been updated, please download the latest from `https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml`.
|
||||
By default, the new compose file uses a fixed version to prevent unintended updates.
|
||||
|
||||
Before updating the file, stop all containers. Then download the file, pull the new containers and start the database.
|
||||
|
||||
```
|
||||
docker-compose down
|
||||
docker-compose pull
|
||||
docker-compose up --no-start
|
||||
docker-compose start redis postgrseql
|
||||
docker-compose run --rm server migrate
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Helm
|
||||
|
||||
A few options have changed:
|
||||
|
||||
- `error_reporting` was changed from a simple boolean to a dictionary:
|
||||
|
||||
```yaml
|
||||
error_reporting:
|
||||
enabled: false
|
||||
environment: customer
|
||||
send_pii: false
|
||||
```
|
||||
|
||||
- The `apm` and `monitoring` blocks have been removed.
|
||||
- `serverReplicas` and `workerReplicas` have been added
|
||||
|
||||
### Upgrading
|
||||
|
||||
This upgrade only applies if you are upgrading from a running 0.9 instance. Passbook detects this on startup, and automatically executes this upgrade.
|
||||
|
||||
Because this upgrade brings the new OAuth2 Provider, the old providers will be lost in the process. Make sure to take note of the providers you want to bring over.
|
||||
|
||||
Another side-effect of this upgrade is the change of OAuth2 URLs, see [here](../providers/oauth2.md).
|
@ -2,7 +2,7 @@ version: '3.7'
|
||||
|
||||
services:
|
||||
chrome:
|
||||
image: selenium/standalone-chrome:3.141.59-20200525
|
||||
image: selenium/standalone-chrome:3.141
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
network_mode: host
|
||||
|
@ -2,7 +2,7 @@ version: '3.7'
|
||||
|
||||
services:
|
||||
chrome:
|
||||
image: selenium/standalone-chrome-debug:3.141.59-20200525
|
||||
image: selenium/standalone-chrome-debug:3.141
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
network_mode: host
|
||||
|
@ -9,7 +9,7 @@ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
sudo npm install -g yarn
|
||||
# Setup python
|
||||
sudo apt install -y python3.8 python3-pip
|
||||
sudo apt install -y python3.9 python3-pip libxmlsec1-dev pkg-config
|
||||
# Setup docker
|
||||
sudo pip3 install pipenv
|
||||
|
||||
|
@ -1,59 +1,40 @@
|
||||
"""Test Enroll flow"""
|
||||
from time import sleep
|
||||
from sys import platform
|
||||
from typing import Any, Dict, Optional
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from django.test import override_settings
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from structlog import get_logger
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from e2e.utils import USER, SeleniumTestCase, retry
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.stages.email.models import EmailStage, EmailTemplates
|
||||
from passbook.stages.identification.models import IdentificationStage
|
||||
from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||
from passbook.stages.user_login.models import UserLoginStage
|
||||
from passbook.stages.user_write.models import UserWriteStage
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestFlowsEnroll(SeleniumTestCase):
|
||||
"""Test Enroll flow"""
|
||||
|
||||
def setUp(self):
|
||||
self.container = self.setup_client()
|
||||
super().setUp()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup test IdP container"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="mailhog/mailhog:v1.0.1",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
healthcheck=Healthcheck(
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "docker.beryju.org/proxy/mailhog/mailhog:v1.0.1",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:8025"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
LOGGER.info("Container failed healthcheck")
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
}
|
||||
|
||||
@retry()
|
||||
def test_enroll_2_step(self):
|
||||
"""Test 2-step enroll flow"""
|
||||
# First stage fields
|
||||
@ -78,16 +59,9 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
||||
)
|
||||
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name="policy-enrollment-password-equals",
|
||||
expression="return request.context['password'] == request.context['password_repeat']",
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.validation_policies.set([password_policy])
|
||||
first_stage.save()
|
||||
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
||||
second_stage.fields.set([name_field, email])
|
||||
@ -131,7 +105,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
|
||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
self.driver.find_element(By.ID, "user-settings").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
@ -146,6 +120,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
"foo@bar.baz",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend")
|
||||
def test_enroll_email(self):
|
||||
"""Test enroll with Email verification"""
|
||||
@ -171,16 +146,9 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
||||
)
|
||||
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name="policy-enrollment-password-equals",
|
||||
expression="return request.context['password'] == request.context['password_repeat']",
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.validation_policies.set([password_policy])
|
||||
first_stage.save()
|
||||
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
||||
second_stage.fields.set([name_field, email])
|
||||
@ -220,30 +188,30 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_name").send_keys("some name")
|
||||
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
sleep(3)
|
||||
# Wait for the success message so we know the email is sent
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, ".pf-c-form > p"))
|
||||
)
|
||||
|
||||
# Open Mailhog
|
||||
self.driver.get("http://localhost:8025")
|
||||
|
||||
# Click on first message
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CLASS_NAME, "msglist-message"))
|
||||
)
|
||||
self.driver.find_element(By.CLASS_NAME, "msglist-message").click()
|
||||
sleep(3)
|
||||
self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane"))
|
||||
self.driver.find_element(By.ID, "confirm").click()
|
||||
self.driver.close()
|
||||
self.driver.switch_to.window(self.driver.window_handles[0])
|
||||
|
||||
# We're now logged in
|
||||
sleep(3)
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||
self.driver.find_element(By.ID, "user-settings").click()
|
||||
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
self.driver.find_element(By.ID, "user-settings").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
|
@ -1,13 +1,18 @@
|
||||
"""test default login flow"""
|
||||
from sys import platform
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from e2e.utils import USER, SeleniumTestCase, retry
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestFlowsLogin(SeleniumTestCase):
|
||||
"""test default login flow"""
|
||||
|
||||
@retry()
|
||||
def test_login(self):
|
||||
"""test default login flow"""
|
||||
self.driver.get(f"{self.live_server_url}/flows/default-authentication-flow/")
|
||||
@ -17,6 +22,6 @@ class TestFlowsLogin(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
self.driver.find_element(By.ID, "user-settings").text,
|
||||
USER().username,
|
||||
)
|
||||
|
144
e2e/test_flows_otp.py
Normal file
144
e2e/test_flows_otp.py
Normal file
@ -0,0 +1,144 @@
|
||||
"""test flow with otp stages"""
|
||||
from base64 import b32decode
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from unittest.case import skipUnless
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from django_otp.oath import TOTP
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase, retry
|
||||
from passbook.flows.models import Flow, FlowStageBinding
|
||||
from passbook.stages.otp_validate.models import OTPValidateStage
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestFlowsOTP(SeleniumTestCase):
|
||||
"""test flow with otp stages"""
|
||||
|
||||
@retry()
|
||||
def test_otp_validate(self):
|
||||
"""test flow with otp stages"""
|
||||
sleep(1)
|
||||
# Setup TOTP Device
|
||||
user = USER()
|
||||
device = TOTPDevice.objects.create(user=user, confirmed=True, digits=6)
|
||||
|
||||
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
||||
# Move the user_login stage to order 3
|
||||
FlowStageBinding.objects.filter(target=flow, order=2).update(order=3)
|
||||
FlowStageBinding.objects.create(
|
||||
target=flow, order=2, stage=OTPValidateStage.objects.create()
|
||||
)
|
||||
|
||||
self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
|
||||
# Get expected token
|
||||
totp = TOTP(device.bin_key, device.step, device.t0, device.digits, device.drift)
|
||||
self.driver.find_element(By.ID, "id_code").send_keys(totp.token())
|
||||
self.driver.find_element(By.ID, "id_code").send_keys(Keys.ENTER)
|
||||
self.wait_for_url(self.url("passbook_core:overview"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "user-settings").text,
|
||||
USER().username,
|
||||
)
|
||||
|
||||
@retry()
|
||||
def test_otp_totp_setup(self):
|
||||
"""test TOTP Setup stage"""
|
||||
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
||||
|
||||
self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "user-settings").text,
|
||||
USER().username,
|
||||
)
|
||||
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
|
||||
self.driver.get(self.url("passbook_core:user-settings"))
|
||||
|
||||
self.driver.find_element(By.LINK_TEXT, "Time-based OTP").click()
|
||||
|
||||
# Remember the current URL as we should end up back here
|
||||
destination_url = self.driver.current_url
|
||||
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-card__body a.pf-c-button"
|
||||
).click()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "qr")))
|
||||
otp_uri = self.driver.find_element(By.ID, "qr").get_attribute("data-otpuri")
|
||||
|
||||
# Parse the OTP URI, extract the secret and get the next token
|
||||
otp_args = urlparse(otp_uri)
|
||||
self.assertEqual(otp_args.scheme, "otpauth")
|
||||
otp_qs = parse_qs(otp_args.query)
|
||||
secret_key = b32decode(otp_qs["secret"][0])
|
||||
|
||||
totp = TOTP(secret_key)
|
||||
|
||||
self.driver.find_element(By.ID, "id_code").send_keys(totp.token())
|
||||
self.driver.find_element(By.ID, "id_code").send_keys(Keys.ENTER)
|
||||
|
||||
self.wait_for_url(destination_url)
|
||||
sleep(1)
|
||||
|
||||
self.assertTrue(TOTPDevice.objects.filter(user=USER(), confirmed=True).exists())
|
||||
|
||||
@retry()
|
||||
def test_otp_static_setup(self):
|
||||
"""test Static OTP Setup stage"""
|
||||
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
||||
|
||||
self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "user-settings").text,
|
||||
USER().username,
|
||||
)
|
||||
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
|
||||
self.driver.find_element(By.ID, "user-settings").click()
|
||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||
|
||||
self.driver.find_element(By.LINK_TEXT, "Static OTP").click()
|
||||
|
||||
# Remember the current URL as we should end up back here
|
||||
destination_url = self.driver.current_url
|
||||
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-card__body a.pf-c-button"
|
||||
).click()
|
||||
token = self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pb-otp-tokens li:nth-child(1)"
|
||||
).text
|
||||
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
|
||||
self.wait_for_url(destination_url)
|
||||
sleep(1)
|
||||
|
||||
self.assertTrue(
|
||||
StaticDevice.objects.filter(user=USER(), confirmed=True).exists()
|
||||
)
|
||||
device = StaticDevice.objects.filter(user=USER(), confirmed=True).first()
|
||||
self.assertTrue(StaticToken.objects.filter(token=token, device=device).exists())
|
@ -1,36 +1,35 @@
|
||||
"""test stage setup flows (password change)"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
from time import sleep
|
||||
from sys import platform
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from e2e.utils import USER, SeleniumTestCase, retry
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.models import Flow, FlowDesignation
|
||||
from passbook.providers.oauth2.generators import generate_client_secret
|
||||
from passbook.stages.password.models import PasswordStage
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestFlowsStageSetup(SeleniumTestCase):
|
||||
"""test stage setup flows"""
|
||||
|
||||
@retry()
|
||||
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,
|
||||
slug="default-password-change",
|
||||
designation=FlowDesignation.STAGE_CONFIGURATION,
|
||||
)
|
||||
|
||||
stages = PasswordStage.objects.filter(name="default-authentication-password")
|
||||
stage = stages.first()
|
||||
stage.change_flow = flow
|
||||
stage = PasswordStage.objects.get(name="default-authentication-password")
|
||||
stage.configure_flow = flow
|
||||
stage.save()
|
||||
|
||||
new_password = "".join(
|
||||
SystemRandom().choice(string.ascii_uppercase + string.digits)
|
||||
for _ in range(8)
|
||||
)
|
||||
new_password = generate_client_secret()
|
||||
|
||||
self.driver.get(
|
||||
f"{self.live_server_url}/flows/default-authentication-flow/?next=%2F"
|
||||
@ -40,7 +39,7 @@ class TestFlowsStageSetup(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
self.driver.find_element(By.ID, "user-settings").click()
|
||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||
self.driver.find_element(By.LINK_TEXT, "Change password").click()
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(new_password)
|
||||
@ -48,7 +47,7 @@ class TestFlowsStageSetup(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_password_repeat").send_keys(new_password)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
|
||||
sleep(2)
|
||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||
# Because USER() is cached, we need to get the user manually here
|
||||
user = User.objects.get(username=USER().username)
|
||||
self.assertTrue(user.check_password(new_password))
|
||||
|
@ -1,14 +1,15 @@
|
||||
"""test OAuth Provider flow"""
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from typing import Any, Dict, Optional
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from structlog import get_logger
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from e2e.utils import USER, SeleniumTestCase, retry
|
||||
from passbook.core.models import Application
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
@ -19,32 +20,29 @@ from passbook.providers.oauth2.generators import (
|
||||
)
|
||||
from passbook.providers.oauth2.models import ClientTypes, OAuth2Provider, ResponseTypes
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
"""test OAuth Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
self.client_id = generate_client_id()
|
||||
self.client_secret = generate_client_secret()
|
||||
self.container = self.setup_client()
|
||||
super().setUp()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
"""Setup client grafana container which we test OAuth against"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="grafana/grafana:7.1.0",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
healthcheck=Healthcheck(
|
||||
return {
|
||||
"image": "docker.beryju.org/proxy/grafana/grafana:7.1.0",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:3000"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
environment={
|
||||
"environment": {
|
||||
"GF_AUTH_GITHUB_ENABLED": "true",
|
||||
"GF_AUTH_GITHUB_ALLOW_SIGN_UP": "true",
|
||||
"GF_AUTH_GITHUB_CLIENT_ID": self.client_id,
|
||||
@ -61,22 +59,11 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
),
|
||||
"GF_LOG_LEVEL": "debug",
|
||||
},
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
LOGGER.info("Container failed healthcheck")
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
}
|
||||
|
||||
@retry()
|
||||
def test_authorization_consent_implied(self):
|
||||
"""test OAuth Provider flow (default authorization flow with implied consent)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
@ -91,7 +78,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
name="Grafana",
|
||||
slug="grafana",
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
@ -103,7 +92,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
|
||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
||||
self.driver.get("http://localhost:3000/profile")
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||
USER().username,
|
||||
@ -127,9 +116,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
USER().username,
|
||||
)
|
||||
|
||||
@retry()
|
||||
def test_authorization_consent_explicit(self):
|
||||
"""test OAuth Provider flow (default authorization flow with explicit consent)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
@ -144,7 +133,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
name="Grafana",
|
||||
slug="grafana",
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
@ -155,23 +146,23 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
|
||||
self.assertIn(
|
||||
sleep(1)
|
||||
|
||||
self.assertEqual(
|
||||
app.name,
|
||||
self.driver.find_element(
|
||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
|
||||
).text,
|
||||
self.driver.find_element(By.ID, "application-name").text,
|
||||
)
|
||||
self.assertEqual(
|
||||
"GitHub Compatibility: Access you Email addresses",
|
||||
self.driver.find_element(
|
||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]"
|
||||
).text,
|
||||
self.driver.find_element(By.ID, "scope-user:email").text,
|
||||
)
|
||||
sleep(1)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
("[type=submit]"),
|
||||
).click()
|
||||
|
||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
||||
self.driver.get("http://localhost:3000/profile")
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||
USER().username,
|
||||
@ -195,9 +186,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
USER().username,
|
||||
)
|
||||
|
||||
@retry()
|
||||
def test_denied(self):
|
||||
"""test OAuth Provider flow (default authorization flow, denied)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
@ -212,7 +203,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
name="Grafana",
|
||||
slug="grafana",
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
negative_policy = ExpressionPolicy.objects.create(
|
||||
@ -227,7 +220,10 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
"Permission denied",
|
||||
|
380
e2e/test_provider_oauth2_grafana.py
Normal file
380
e2e/test_provider_oauth2_grafana.py
Normal file
@ -0,0 +1,380 @@
|
||||
"""test OAuth2 OpenID Provider flow"""
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from typing import Any, Dict, Optional
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from structlog import get_logger
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase, retry
|
||||
from passbook.core.models import Application
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.providers.oauth2.constants import (
|
||||
SCOPE_OPENID,
|
||||
SCOPE_OPENID_EMAIL,
|
||||
SCOPE_OPENID_PROFILE,
|
||||
)
|
||||
from passbook.providers.oauth2.generators import (
|
||||
generate_client_id,
|
||||
generate_client_secret,
|
||||
)
|
||||
from passbook.providers.oauth2.models import (
|
||||
ClientTypes,
|
||||
OAuth2Provider,
|
||||
ResponseTypes,
|
||||
ScopeMapping,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
APPLICATION_SLUG = "grafana"
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
"""test OAuth with OAuth Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
self.client_id = generate_client_id()
|
||||
self.client_secret = generate_client_secret()
|
||||
super().setUp()
|
||||
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "docker.beryju.org/proxy/grafana/grafana:7.1.0",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:3000"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
"environment": {
|
||||
"GF_AUTH_GENERIC_OAUTH_ENABLED": "true",
|
||||
"GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id,
|
||||
"GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret,
|
||||
"GF_AUTH_GENERIC_OAUTH_SCOPES": "openid email profile",
|
||||
"GF_AUTH_GENERIC_OAUTH_AUTH_URL": (
|
||||
self.url("passbook_providers_oauth2:authorize")
|
||||
),
|
||||
"GF_AUTH_GENERIC_OAUTH_TOKEN_URL": (
|
||||
self.url("passbook_providers_oauth2:token")
|
||||
),
|
||||
"GF_AUTH_GENERIC_OAUTH_API_URL": (
|
||||
self.url("passbook_providers_oauth2:userinfo")
|
||||
),
|
||||
"GF_AUTH_SIGNOUT_REDIRECT_URL": (
|
||||
self.url(
|
||||
"passbook_providers_oauth2:end-session",
|
||||
application_slug=APPLICATION_SLUG,
|
||||
)
|
||||
),
|
||||
"GF_LOG_LEVEL": "debug",
|
||||
},
|
||||
}
|
||||
|
||||
@retry()
|
||||
def test_redirect_uri_error(self):
|
||||
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/",
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||
)
|
||||
)
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="Grafana",
|
||||
slug=APPLICATION_SLUG,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
sleep(2)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "pf-c-title").text,
|
||||
"Redirect URI Error",
|
||||
)
|
||||
|
||||
@retry()
|
||||
def test_authorization_consent_implied(self):
|
||||
"""test OpenID Provider flow (default authorization flow with implied consent)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||
)
|
||||
)
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="Grafana",
|
||||
slug=APPLICATION_SLUG,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||
self.driver.get("http://localhost:3000/profile")
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
|
||||
@retry()
|
||||
def test_authorization_logout(self):
|
||||
"""test OpenID Provider flow with logout"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||
)
|
||||
)
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="Grafana",
|
||||
slug=APPLICATION_SLUG,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||
self.driver.get("http://localhost:3000/profile")
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.driver.get("http://localhost:3000/logout")
|
||||
self.wait_for_url(
|
||||
self.url(
|
||||
"passbook_providers_oauth2:end-session",
|
||||
application_slug=APPLICATION_SLUG,
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.ID, "logout").click()
|
||||
|
||||
@retry()
|
||||
def test_authorization_consent_explicit(self):
|
||||
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||
)
|
||||
)
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
name="Grafana",
|
||||
slug=APPLICATION_SLUG,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
|
||||
self.assertEqual(
|
||||
app.name,
|
||||
self.driver.find_element(By.ID, "application-name").text,
|
||||
)
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
|
||||
)
|
||||
sleep(1)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
||||
|
||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||
self.driver.get("http://localhost:3000/profile")
|
||||
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
|
||||
@retry()
|
||||
def test_authorization_denied(self):
|
||||
"""test OpenID Provider flow (default authorization with access deny)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||
)
|
||||
)
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
name="Grafana",
|
||||
slug=APPLICATION_SLUG,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
negative_policy = ExpressionPolicy.objects.create(
|
||||
name="negative-static", expression="return False"
|
||||
)
|
||||
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
"Permission denied",
|
||||
)
|
@ -1,5 +1,8 @@
|
||||
"""test OAuth2 OpenID Provider flow"""
|
||||
from json import loads
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
@ -9,7 +12,7 @@ from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from structlog import get_logger
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from e2e.utils import USER, SeleniumTestCase, retry
|
||||
from passbook.core.models import Application
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.flows.models import Flow
|
||||
@ -34,43 +37,35 @@ from passbook.providers.oauth2.models import (
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
"""test OAuth with OpenID Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
self.client_id = generate_client_id()
|
||||
self.client_secret = generate_client_secret()
|
||||
self.container = self.setup_client()
|
||||
self.application_slug = "test"
|
||||
super().setUp()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup client grafana container which we test OIDC against"""
|
||||
"""Setup client saml-sp container which we test SAML against"""
|
||||
sleep(1)
|
||||
client: DockerClient = from_env()
|
||||
client.images.pull("beryju/oidc-test-client")
|
||||
container = client.containers.run(
|
||||
image="grafana/grafana:7.1.0",
|
||||
image="docker.beryju.org/proxy/beryju/oidc-test-client",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
healthcheck=Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:3000"],
|
||||
test=["CMD", "wget", "--spider", "http://localhost:9009/health"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
environment={
|
||||
"GF_AUTH_GENERIC_OAUTH_ENABLED": "true",
|
||||
"GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id,
|
||||
"GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret,
|
||||
"GF_AUTH_GENERIC_OAUTH_SCOPES": "openid email profile",
|
||||
"GF_AUTH_GENERIC_OAUTH_AUTH_URL": (
|
||||
self.url("passbook_providers_oauth2:authorize")
|
||||
),
|
||||
"GF_AUTH_GENERIC_OAUTH_TOKEN_URL": (
|
||||
self.url("passbook_providers_oauth2:token")
|
||||
),
|
||||
"GF_AUTH_GENERIC_OAUTH_API_URL": (
|
||||
self.url("passbook_providers_oauth2:userinfo")
|
||||
),
|
||||
"GF_LOG_LEVEL": "debug",
|
||||
"OIDC_CLIENT_ID": self.client_id,
|
||||
"OIDC_CLIENT_SECRET": self.client_secret,
|
||||
"OIDC_PROVIDER": f"{self.live_server_url}/application/o/{self.application_slug}/",
|
||||
},
|
||||
)
|
||||
while True:
|
||||
@ -81,10 +76,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
LOGGER.info("Container failed healthcheck")
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
|
||||
@retry()
|
||||
def test_redirect_uri_error(self):
|
||||
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
|
||||
sleep(1)
|
||||
@ -93,12 +85,12 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
name=self.application_slug,
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/",
|
||||
redirect_uris="http://localhost:9009/",
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
)
|
||||
@ -109,11 +101,14 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
)
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
name=self.application_slug,
|
||||
slug=self.application_slug,
|
||||
provider=provider,
|
||||
)
|
||||
self.container = self.setup_client()
|
||||
|
||||
self.driver.get("http://localhost:9009")
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
@ -125,6 +120,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
"Redirect URI Error",
|
||||
)
|
||||
|
||||
@retry()
|
||||
def test_authorization_consent_implied(self):
|
||||
"""test OpenID Provider flow (default authorization flow with implied consent)"""
|
||||
sleep(1)
|
||||
@ -133,12 +129,12 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
name=self.application_slug,
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
redirect_uris="http://localhost:9009/auth/callback",
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
)
|
||||
@ -149,40 +145,33 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
)
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
name=self.application_slug,
|
||||
slug=self.application_slug,
|
||||
provider=provider,
|
||||
)
|
||||
self.container = self.setup_client()
|
||||
|
||||
self.driver.get("http://localhost:9009")
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre")))
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
|
||||
self.assertEqual(body["IDTokenClaims"]["nickname"], USER().username)
|
||||
self.assertEqual(body["UserInfo"]["nickname"], USER().username)
|
||||
|
||||
self.assertEqual(body["IDTokenClaims"]["name"], USER().name)
|
||||
self.assertEqual(body["UserInfo"]["name"], USER().name)
|
||||
|
||||
self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
|
||||
self.assertEqual(body["UserInfo"]["email"], USER().email)
|
||||
|
||||
@retry()
|
||||
def test_authorization_consent_explicit(self):
|
||||
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
|
||||
sleep(1)
|
||||
@ -191,14 +180,14 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
name=self.application_slug,
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
redirect_uris="http://localhost:9009/auth/callback",
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
@ -207,22 +196,23 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
)
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
name=self.application_slug,
|
||||
slug=self.application_slug,
|
||||
provider=provider,
|
||||
)
|
||||
self.container = self.setup_client()
|
||||
|
||||
self.driver.get("http://localhost:9009")
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
|
||||
self.assertIn(
|
||||
self.assertEqual(
|
||||
app.name,
|
||||
self.driver.find_element(
|
||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
|
||||
).text,
|
||||
self.driver.find_element(By.ID, "application-name").text,
|
||||
)
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
|
||||
@ -230,35 +220,19 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
sleep(1)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.XPATH, "//a[contains(@href, '/profile')]")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre")))
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
|
||||
self.assertEqual(body["IDTokenClaims"]["nickname"], USER().username)
|
||||
self.assertEqual(body["UserInfo"]["nickname"], USER().username)
|
||||
|
||||
self.assertEqual(body["IDTokenClaims"]["name"], USER().name)
|
||||
self.assertEqual(body["UserInfo"]["name"], USER().name)
|
||||
|
||||
self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
|
||||
self.assertEqual(body["UserInfo"]["email"], USER().email)
|
||||
|
||||
@retry()
|
||||
def test_authorization_denied(self):
|
||||
"""test OpenID Provider flow (default authorization with access deny)"""
|
||||
sleep(1)
|
||||
@ -267,14 +241,14 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
name=self.application_slug,
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
redirect_uris="http://localhost:9009/auth/callback",
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
@ -283,21 +257,28 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
)
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
name=self.application_slug,
|
||||
slug=self.application_slug,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
negative_policy = ExpressionPolicy.objects.create(
|
||||
name="negative-static", expression="return False"
|
||||
)
|
||||
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
|
||||
self.container = self.setup_client()
|
||||
self.driver.get("http://localhost:9009")
|
||||
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
"Permission denied",
|
||||
|
157
e2e/test_provider_proxy.py
Normal file
157
e2e/test_provider_proxy.py
Normal file
@ -0,0 +1,157 @@
|
||||
"""Proxy and Outpost e2e tests"""
|
||||
from dataclasses import asdict
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from typing import Any, Dict, Optional
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from channels.testing import ChannelsLiveServerTestCase
|
||||
from docker.client import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase, retry
|
||||
from passbook import __version__
|
||||
from passbook.core.models import Application
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
Outpost,
|
||||
OutpostConfig,
|
||||
OutpostType,
|
||||
)
|
||||
from passbook.providers.proxy.models import ProxyProvider
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderProxy(SeleniumTestCase):
|
||||
"""Proxy and Outpost e2e tests"""
|
||||
|
||||
proxy_container: Container
|
||||
|
||||
def tearDown(self) -> None:
|
||||
super().tearDown()
|
||||
self.proxy_container.kill()
|
||||
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "docker.beryju.org/proxy/traefik/whoami:latest",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
}
|
||||
|
||||
def start_proxy(self, outpost: Outpost) -> Container:
|
||||
"""Start proxy container based on outpost created"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image=f"beryju/passbook-proxy:{__version__}",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
environment={
|
||||
"PASSBOOK_HOST": self.live_server_url,
|
||||
"PASSBOOK_TOKEN": outpost.token.key,
|
||||
},
|
||||
)
|
||||
return container
|
||||
|
||||
@retry()
|
||||
def test_proxy_simple(self):
|
||||
"""Test simple outpost setup with single provider"""
|
||||
proxy: ProxyProvider = ProxyProvider.objects.create(
|
||||
name="proxy_provider",
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
internal_host="http://localhost:80",
|
||||
external_host="http://localhost:4180",
|
||||
)
|
||||
# Ensure OAuth2 Params are set
|
||||
proxy.set_oauth_defaults()
|
||||
proxy.save()
|
||||
# we need to create an application to actually access the proxy
|
||||
Application.objects.create(name="proxy", slug="proxy", provider=proxy)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name="proxy_outpost",
|
||||
type=OutpostType.PROXY,
|
||||
)
|
||||
outpost.providers.add(proxy)
|
||||
outpost.save()
|
||||
|
||||
self.proxy_container = self.start_proxy(outpost)
|
||||
|
||||
# Wait until outpost healthcheck succeeds
|
||||
healthcheck_retries = 0
|
||||
while healthcheck_retries < 50:
|
||||
if len(outpost.state) > 0:
|
||||
state = outpost.state[0]
|
||||
if state.last_seen:
|
||||
break
|
||||
healthcheck_retries += 1
|
||||
sleep(0.5)
|
||||
|
||||
self.driver.get("http://localhost:4180")
|
||||
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
|
||||
sleep(1)
|
||||
|
||||
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||
self.assertIn("X-Forwarded-Preferred-Username: pbadmin", full_body_text)
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
||||
"""Test Proxy connectivity over websockets"""
|
||||
|
||||
@retry()
|
||||
def test_proxy_connectivity(self):
|
||||
"""Test proxy connectivity over websocket"""
|
||||
SeleniumTestCase().apply_default_data()
|
||||
proxy: ProxyProvider = ProxyProvider.objects.create(
|
||||
name="proxy_provider",
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
internal_host="http://localhost:80",
|
||||
external_host="http://localhost:4180",
|
||||
)
|
||||
# Ensure OAuth2 Params are set
|
||||
proxy.set_oauth_defaults()
|
||||
proxy.save()
|
||||
# we need to create an application to actually access the proxy
|
||||
Application.objects.create(name="proxy", slug="proxy", provider=proxy)
|
||||
service_connection = DockerServiceConnection.objects.get(local=True)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name="proxy_outpost",
|
||||
type=OutpostType.PROXY,
|
||||
service_connection=service_connection,
|
||||
_config=asdict(
|
||||
OutpostConfig(passbook_host=self.live_server_url, log_level="debug")
|
||||
),
|
||||
)
|
||||
outpost.providers.add(proxy)
|
||||
outpost.save()
|
||||
|
||||
# Wait until outpost healthcheck succeeds
|
||||
healthcheck_retries = 0
|
||||
while healthcheck_retries < 50:
|
||||
if len(outpost.state) > 0:
|
||||
state = outpost.state[0]
|
||||
if state.last_seen and state.version:
|
||||
break
|
||||
healthcheck_retries += 1
|
||||
sleep(0.5)
|
||||
|
||||
state = outpost.state
|
||||
self.assertTrue(len(state), 1)
|
||||
self.assertEqual(state[0].version, __version__)
|
||||
|
||||
# Make sure to delete the outpost to remove the container
|
||||
outpost.delete()
|
@ -1,14 +1,18 @@
|
||||
"""test SAML Provider flow"""
|
||||
from json import loads
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from structlog import get_logger
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from e2e.utils import USER, SeleniumTestCase, retry
|
||||
from passbook.core.models import Application
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.flows.models import Flow
|
||||
@ -23,6 +27,7 @@ from passbook.providers.saml.models import (
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderSAML(SeleniumTestCase):
|
||||
"""test SAML Provider flow"""
|
||||
|
||||
@ -31,8 +36,9 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
def setup_client(self, provider: SAMLProvider) -> Container:
|
||||
"""Setup client saml-sp container which we test SAML against"""
|
||||
client: DockerClient = from_env()
|
||||
client.images.pull("beryju/oidc-test-client")
|
||||
container = client.containers.run(
|
||||
image="beryju/saml-test-sp",
|
||||
image="docker.beryju.org/proxy/beryju/saml-test-sp",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
@ -60,10 +66,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
LOGGER.info("Container failed healthcheck")
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
|
||||
@retry()
|
||||
def test_sp_initiated_implicit(self):
|
||||
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
|
||||
# Bootstrap all needed objects
|
||||
@ -82,7 +85,9 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="SAML", slug="passbook-saml", provider=provider,
|
||||
name="SAML",
|
||||
slug="passbook-saml",
|
||||
provider=provider,
|
||||
)
|
||||
self.container = self.setup_client(provider)
|
||||
self.driver.get("http://localhost:9009")
|
||||
@ -92,11 +97,16 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "/html/body/pre").text,
|
||||
f"Hello, {USER().name}!",
|
||||
)
|
||||
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
|
||||
self.assertEqual(body["attr"]["cn"], [USER().name])
|
||||
self.assertEqual(body["attr"]["displayName"], [USER().username])
|
||||
self.assertEqual(body["attr"]["eduPersonPrincipalName"], [USER().email])
|
||||
self.assertEqual(body["attr"]["mail"], [USER().email])
|
||||
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
|
||||
|
||||
@retry()
|
||||
def test_sp_initiated_explicit(self):
|
||||
"""test SAML Provider flow SP-initiated flow (explicit consent)"""
|
||||
# Bootstrap all needed objects
|
||||
@ -115,7 +125,9 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
name="SAML", slug="passbook-saml", provider=provider,
|
||||
name="SAML",
|
||||
slug="passbook-saml",
|
||||
provider=provider,
|
||||
)
|
||||
self.container = self.setup_client(provider)
|
||||
self.driver.get("http://localhost:9009")
|
||||
@ -124,20 +136,23 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.assertIn(
|
||||
self.assertEqual(
|
||||
app.name,
|
||||
self.driver.find_element(
|
||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
|
||||
).text,
|
||||
self.driver.find_element(By.ID, "application-name").text,
|
||||
)
|
||||
sleep(1)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "/html/body/pre").text,
|
||||
f"Hello, {USER().name}!",
|
||||
)
|
||||
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
|
||||
self.assertEqual(body["attr"]["cn"], [USER().name])
|
||||
self.assertEqual(body["attr"]["displayName"], [USER().username])
|
||||
self.assertEqual(body["attr"]["eduPersonPrincipalName"], [USER().email])
|
||||
self.assertEqual(body["attr"]["mail"], [USER().email])
|
||||
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
|
||||
|
||||
@retry()
|
||||
def test_idp_initiated_implicit(self):
|
||||
"""test SAML Provider flow IdP-initiated flow (implicit consent)"""
|
||||
# Bootstrap all needed objects
|
||||
@ -156,7 +171,9 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="SAML", slug="passbook-saml", provider=provider,
|
||||
name="SAML",
|
||||
slug="passbook-saml",
|
||||
provider=provider,
|
||||
)
|
||||
self.container = self.setup_client(provider)
|
||||
self.driver.get(
|
||||
@ -170,12 +187,18 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
sleep(1)
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "/html/body/pre").text,
|
||||
f"Hello, {USER().name}!",
|
||||
)
|
||||
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
|
||||
self.assertEqual(body["attr"]["cn"], [USER().name])
|
||||
self.assertEqual(body["attr"]["displayName"], [USER().username])
|
||||
self.assertEqual(body["attr"]["eduPersonPrincipalName"], [USER().email])
|
||||
self.assertEqual(body["attr"]["mail"], [USER().email])
|
||||
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
|
||||
|
||||
@retry()
|
||||
def test_sp_initiated_denied(self):
|
||||
"""test SAML Provider flow SP-initiated flow (Policy denies access)"""
|
||||
# Bootstrap all needed objects
|
||||
@ -197,7 +220,9 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
name="SAML", slug="passbook-saml", provider=provider,
|
||||
name="SAML",
|
||||
slug="passbook-saml",
|
||||
provider=provider,
|
||||
)
|
||||
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
|
||||
self.container = self.setup_client(provider)
|
||||
@ -207,7 +232,10 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
"Permission denied",
|
||||
|
343
e2e/test_source_oauth.py
Normal file
343
e2e/test_source_oauth.py
Normal file
@ -0,0 +1,343 @@
|
||||
"""test OAuth Source"""
|
||||
from os.path import abspath
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from typing import Any, Dict, Optional
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from django.test import override_settings
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from structlog import get_logger
|
||||
from yaml import safe_dump
|
||||
|
||||
from e2e.utils import SeleniumTestCase, retry
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.providers.oauth2.generators import (
|
||||
generate_client_id,
|
||||
generate_client_secret,
|
||||
)
|
||||
from passbook.sources.oauth.models import OAuthSource
|
||||
|
||||
CONFIG_PATH = "/tmp/dex.yml"
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestSourceOAuth2(SeleniumTestCase):
|
||||
"""test OAuth Source flow"""
|
||||
|
||||
container: Container
|
||||
|
||||
def setUp(self):
|
||||
self.client_secret = generate_client_secret()
|
||||
self.prepare_dex_config()
|
||||
super().setUp()
|
||||
|
||||
def prepare_dex_config(self):
|
||||
"""Since Dex does not document which environment
|
||||
variables can be used to configure clients"""
|
||||
config = {
|
||||
"enablePasswordDB": True,
|
||||
"issuer": "http://127.0.0.1:5556/dex",
|
||||
"logger": {"level": "debug"},
|
||||
"staticClients": [
|
||||
{
|
||||
"id": "example-app",
|
||||
"name": "Example App",
|
||||
"redirectURIs": [
|
||||
self.url(
|
||||
"passbook_sources_oauth:oauth-client-callback",
|
||||
source_slug="dex",
|
||||
)
|
||||
],
|
||||
"secret": self.client_secret,
|
||||
}
|
||||
],
|
||||
"staticPasswords": [
|
||||
{
|
||||
"email": "admin@example.com",
|
||||
# hash for password
|
||||
"hash": "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W",
|
||||
"userID": "08a8684b-db88-4b73-90a9-3cd1661f5466",
|
||||
"username": "admin",
|
||||
}
|
||||
],
|
||||
"storage": {"config": {"file": "/tmp/dex.db"}, "type": "sqlite3"},
|
||||
"web": {"http": "0.0.0.0:5556"},
|
||||
}
|
||||
with open(CONFIG_PATH, "w+") as _file:
|
||||
safe_dump(config, _file)
|
||||
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "quay.io/dexidp/dex:v2.24.0",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
"command": "serve /config.yml",
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
"volumes": {abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro"}},
|
||||
}
|
||||
|
||||
def create_objects(self):
|
||||
"""Create required objects"""
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
|
||||
OAuthSource.objects.create( # nosec
|
||||
name="dex",
|
||||
slug="dex",
|
||||
authentication_flow=authentication_flow,
|
||||
enrollment_flow=enrollment_flow,
|
||||
provider_type="openid-connect",
|
||||
authorization_url="http://127.0.0.1:5556/dex/auth",
|
||||
access_token_url="http://127.0.0.1:5556/dex/token",
|
||||
profile_url="http://127.0.0.1:5556/dex/userinfo",
|
||||
consumer_key="example-app",
|
||||
consumer_secret=self.client_secret,
|
||||
)
|
||||
|
||||
@retry()
|
||||
def test_oauth_enroll(self):
|
||||
"""test OAuth Source With With OIDC"""
|
||||
self.create_objects()
|
||||
self.driver.get(self.live_server_url)
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.NAME, "username")))
|
||||
# At this point we've been redirected back
|
||||
# and we're asked for the username
|
||||
self.driver.find_element(By.NAME, "username").click()
|
||||
self.driver.find_element(By.NAME, "username").send_keys("foo")
|
||||
self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||
self.driver.get(self.url("passbook_core:user-settings"))
|
||||
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "user-settings").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
|
||||
"admin",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||
"admin@example.com",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@override_settings(SESSION_COOKIE_SAMESITE="strict")
|
||||
def test_oauth_samesite_strict(self):
|
||||
"""test OAuth Source With SameSite set to strict
|
||||
(=will fail because session is not carried over)"""
|
||||
self.create_objects()
|
||||
self.driver.get(self.live_server_url)
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, ".pf-c-alert__title"))
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-alert__title").text,
|
||||
"Authentication Failed.",
|
||||
)
|
||||
|
||||
@retry()
|
||||
def test_oauth_enroll_auth(self):
|
||||
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
|
||||
self.test_oauth_enroll()
|
||||
# We're logged in at the end of this, log out and re-login
|
||||
self.driver.find_element(By.ID, "logout").click()
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
)
|
||||
)
|
||||
sleep(1)
|
||||
self.driver.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
).click()
|
||||
sleep(1)
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||
self.driver.get(self.url("passbook_core:user-settings"))
|
||||
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "user-settings").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
|
||||
"admin",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||
"admin@example.com",
|
||||
)
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestSourceOAuth1(SeleniumTestCase):
|
||||
"""Test OAuth1 Source"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.client_id = generate_client_id()
|
||||
self.client_secret = generate_client_secret()
|
||||
self.source_slug = "oauth1-test"
|
||||
super().setUp()
|
||||
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "docker.beryju.org/proxy/beryju/oauth1-test-server",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
"environment": {
|
||||
"OAUTH1_CLIENT_ID": self.client_id,
|
||||
"OAUTH1_CLIENT_SECRET": self.client_secret,
|
||||
"OAUTH1_REDIRECT_URI": (
|
||||
self.url(
|
||||
"passbook_sources_oauth:oauth-client-callback",
|
||||
source_slug=self.source_slug,
|
||||
)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
def create_objects(self):
|
||||
"""Create required objects"""
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
|
||||
OAuthSource.objects.create( # nosec
|
||||
name="oauth1",
|
||||
slug=self.source_slug,
|
||||
authentication_flow=authentication_flow,
|
||||
enrollment_flow=enrollment_flow,
|
||||
provider_type="twitter",
|
||||
request_token_url="http://localhost:5000/oauth/request_token",
|
||||
access_token_url="http://localhost:5000/oauth/access_token",
|
||||
authorization_url="http://localhost:5000/oauth/authorize",
|
||||
profile_url="http://localhost:5000/api/me",
|
||||
consumer_key=self.client_id,
|
||||
consumer_secret=self.client_secret,
|
||||
)
|
||||
|
||||
@retry()
|
||||
def test_oauth_enroll(self):
|
||||
"""test OAuth Source With With OIDC"""
|
||||
self.create_objects()
|
||||
self.driver.get(self.live_server_url)
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.NAME, "username")))
|
||||
self.driver.find_element(By.NAME, "username").send_keys("example-user")
|
||||
self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[name='confirm']"))
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[name='confirm']").click()
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
sleep(2)
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||
self.driver.get(self.url("passbook_core:user-settings"))
|
||||
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "user-settings").text,
|
||||
"example-user",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"),
|
||||
"example-user",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
|
||||
"test name",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||
"foo@example.com",
|
||||
)
|
@ -1,15 +1,16 @@
|
||||
"""test SAML Source"""
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from typing import Any, Dict, Optional
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from structlog import get_logger
|
||||
|
||||
from e2e.utils import SeleniumTestCase
|
||||
from e2e.utils import SeleniumTestCase, retry
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.sources.saml.models import SAMLBindingTypes, SAMLSource
|
||||
@ -68,53 +69,39 @@ Sm75WXsflOxuTn08LbgGc4s=
|
||||
-----END PRIVATE KEY-----"""
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestSourceSAML(SeleniumTestCase):
|
||||
"""test SAML Source flow"""
|
||||
|
||||
def setUp(self):
|
||||
self.container = self.setup_client()
|
||||
super().setUp()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup test IdP container"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="kristophjunge/test-saml-idp:1.15",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
healthcheck=Healthcheck(
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "docker.beryju.org/proxy/kristophjunge/test-saml-idp:1.15",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "curl", "http://localhost:8080"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
environment={
|
||||
"environment": {
|
||||
"SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id",
|
||||
"SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE": (
|
||||
f"{self.live_server_url}/source/saml/saml-idp-test/acs/"
|
||||
),
|
||||
},
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
LOGGER.info("Container failed healthcheck")
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
}
|
||||
|
||||
@retry()
|
||||
def test_idp_redirect(self):
|
||||
"""test SAML Source With redirect binding"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
keypair = CertificateKeyPair.objects.create(
|
||||
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
|
||||
name="test-idp-cert",
|
||||
certificate_data=IDP_CERT,
|
||||
key_data=IDP_KEY,
|
||||
)
|
||||
|
||||
SAMLSource.objects.create(
|
||||
@ -146,12 +133,8 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||
self.driver.get(self.url("passbook_core:user-settings"))
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
||||
@ -159,14 +142,16 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
||||
)
|
||||
|
||||
@retry()
|
||||
def test_idp_post(self):
|
||||
"""test SAML Source With post binding"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
keypair = CertificateKeyPair.objects.create(
|
||||
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
|
||||
name="test-idp-cert",
|
||||
certificate_data=IDP_CERT,
|
||||
key_data=IDP_KEY,
|
||||
)
|
||||
|
||||
SAMLSource.objects.create(
|
||||
@ -200,12 +185,8 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||
self.driver.get(self.url("passbook_core:user-settings"))
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
||||
@ -213,14 +194,16 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
||||
)
|
||||
|
||||
@retry()
|
||||
def test_idp_post_auto(self):
|
||||
"""test SAML Source With post binding (auto redirect)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
keypair = CertificateKeyPair.objects.create(
|
||||
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
|
||||
name="test-idp-cert",
|
||||
certificate_data=IDP_CERT,
|
||||
key_data=IDP_KEY,
|
||||
)
|
||||
|
||||
SAMLSource.objects.create(
|
||||
@ -252,12 +235,8 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||
self.driver.get(self.url("passbook_core:user-settings"))
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
||||
|
@ -1,215 +0,0 @@
|
||||
"""test OAuth Source"""
|
||||
from os.path import abspath
|
||||
from time import sleep
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from structlog import get_logger
|
||||
from yaml import safe_dump
|
||||
|
||||
from e2e.utils import SeleniumTestCase
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.providers.oauth2.generators import generate_client_secret
|
||||
from passbook.sources.oauth.models import OAuthSource
|
||||
|
||||
TOKEN_URL = "http://127.0.0.1:5556/dex/token"
|
||||
CONFIG_PATH = "/tmp/dex.yml"
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class TestSourceOAuth(SeleniumTestCase):
|
||||
"""test OAuth Source flow"""
|
||||
|
||||
container: Container
|
||||
|
||||
def setUp(self):
|
||||
self.client_secret = generate_client_secret()
|
||||
self.container = self.setup_client()
|
||||
super().setUp()
|
||||
|
||||
def prepare_dex_config(self):
|
||||
"""Since Dex does not document which environment
|
||||
variables can be used to configure clients"""
|
||||
config = {
|
||||
"enablePasswordDB": True,
|
||||
"issuer": "http://127.0.0.1:5556/dex",
|
||||
"logger": {"level": "debug"},
|
||||
"staticClients": [
|
||||
{
|
||||
"id": "example-app",
|
||||
"name": "Example App",
|
||||
"redirectURIs": [
|
||||
self.url(
|
||||
"passbook_sources_oauth:oauth-client-callback",
|
||||
source_slug="dex",
|
||||
)
|
||||
],
|
||||
"secret": self.client_secret,
|
||||
}
|
||||
],
|
||||
"staticPasswords": [
|
||||
{
|
||||
"email": "admin@example.com",
|
||||
# hash for password
|
||||
"hash": "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W",
|
||||
"userID": "08a8684b-db88-4b73-90a9-3cd1661f5466",
|
||||
"username": "admin",
|
||||
}
|
||||
],
|
||||
"storage": {"config": {"file": "/tmp/dex.db"}, "type": "sqlite3"},
|
||||
"web": {"http": "0.0.0.0:5556"},
|
||||
}
|
||||
with open(CONFIG_PATH, "w+") as _file:
|
||||
safe_dump(config, _file)
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup test Dex container"""
|
||||
self.prepare_dex_config()
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="quay.io/dexidp/dex:v2.24.0",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
command="serve /config.yml",
|
||||
healthcheck=Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
volumes={abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro"}},
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
LOGGER.info("Container failed healthcheck")
|
||||
sleep(1)
|
||||
|
||||
def create_objects(self):
|
||||
"""Create required objects"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
|
||||
OAuthSource.objects.create(
|
||||
name="dex",
|
||||
slug="dex",
|
||||
authentication_flow=authentication_flow,
|
||||
enrollment_flow=enrollment_flow,
|
||||
provider_type="openid-connect",
|
||||
authorization_url="http://127.0.0.1:5556/dex/auth",
|
||||
access_token_url=TOKEN_URL,
|
||||
profile_url="http://127.0.0.1:5556/dex/userinfo",
|
||||
consumer_key="example-app",
|
||||
consumer_secret=self.client_secret,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
|
||||
def test_oauth_enroll(self):
|
||||
"""test OAuth Source With With OIDC"""
|
||||
self.create_objects()
|
||||
self.driver.get(self.live_server_url)
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
|
||||
# At this point we've been redirected back
|
||||
# and we're asked for the username
|
||||
self.driver.find_element(By.NAME, "username").click()
|
||||
self.driver.find_element(By.NAME, "username").send_keys("foo")
|
||||
self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
|
||||
self.driver.find_element(By.LINK_TEXT, "foo").click()
|
||||
|
||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"), "admin",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||
"admin@example.com",
|
||||
)
|
||||
|
||||
def test_oauth_enroll_auth(self):
|
||||
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
|
||||
self.test_oauth_enroll()
|
||||
# We're logged in at the end of this, log out and re-login
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[aria-label=logout]").click()
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
|
||||
self.driver.find_element(By.LINK_TEXT, "foo").click()
|
||||
|
||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"), "admin",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||
"admin@example.com",
|
||||
)
|
74
e2e/utils.py
74
e2e/utils.py
@ -1,17 +1,22 @@
|
||||
"""passbook e2e testing utilities"""
|
||||
from functools import lru_cache
|
||||
from functools import wraps
|
||||
from glob import glob
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from inspect import getmembers, isfunction
|
||||
from os import environ, makedirs
|
||||
from time import time
|
||||
from time import sleep, time
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
||||
from django.db import connection, transaction
|
||||
from django.db.utils import IntegrityError
|
||||
from django.shortcuts import reverse
|
||||
from django.test.testcases import TransactionTestCase
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from selenium import webdriver
|
||||
from selenium.common.exceptions import NoSuchElementException, TimeoutException
|
||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
@ -20,7 +25,6 @@ from structlog import get_logger
|
||||
from passbook.core.models import User
|
||||
|
||||
|
||||
@lru_cache
|
||||
# pylint: disable=invalid-name
|
||||
def USER() -> User: # noqa
|
||||
"""Cached function that always returns pbadmin"""
|
||||
@ -30,15 +34,38 @@ def USER() -> User: # noqa
|
||||
class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
|
||||
|
||||
container: Optional[Container] = None
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
makedirs("selenium_screenshots/", exist_ok=True)
|
||||
self.driver = self._get_driver()
|
||||
self.driver.maximize_window()
|
||||
self.driver.implicitly_wait(30)
|
||||
self.wait = WebDriverWait(self.driver, 50)
|
||||
self.wait = WebDriverWait(self.driver, 60)
|
||||
self.apply_default_data()
|
||||
self.logger = get_logger()
|
||||
if specs := self.get_container_specs():
|
||||
self.container = self._start_container(specs)
|
||||
|
||||
def _start_container(self, specs: Dict[str, Any]) -> Container:
|
||||
client: DockerClient = from_env()
|
||||
client.images.pull(specs["image"])
|
||||
container = client.containers.run(**specs)
|
||||
if "healthcheck" not in specs:
|
||||
return container
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
self.logger.info("Container failed healthcheck")
|
||||
sleep(1)
|
||||
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
"""Optionally get container specs which will launched on setup, wait for the container to
|
||||
be healthy, and deleted again on tearDown"""
|
||||
return None
|
||||
|
||||
def _get_driver(self) -> WebDriver:
|
||||
return webdriver.Remote(
|
||||
@ -57,6 +84,8 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
self.logger.warning(
|
||||
line["message"], source=line["source"], level=line["level"]
|
||||
)
|
||||
if self.container:
|
||||
self.container.kill()
|
||||
self.driver.quit()
|
||||
super().tearDown()
|
||||
|
||||
@ -97,3 +126,40 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
func(apps, schema_editor)
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
|
||||
def retry(max_retires=3, exceptions=None):
|
||||
"""Retry test multiple times. Default to catching Selenium Timeout Exception"""
|
||||
|
||||
if not exceptions:
|
||||
exceptions = [TimeoutException, NoSuchElementException]
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
def retry_actual(func: Callable):
|
||||
"""Retry test multiple times"""
|
||||
count = 1
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self: TransactionTestCase, *args, **kwargs):
|
||||
"""Run test again if we're below max_retries, including tearDown and
|
||||
setUp. Otherwise raise the error"""
|
||||
nonlocal count
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
# pylint: disable=catching-non-exception
|
||||
except tuple(exceptions) as exc:
|
||||
count += 1
|
||||
if count > max_retires:
|
||||
logger.debug("Exceeded retry count", exc=exc, test=self)
|
||||
# pylint: disable=raising-non-exception
|
||||
raise exc
|
||||
logger.debug("Retrying on error", exc=exc, test=self)
|
||||
self.tearDown()
|
||||
self._post_teardown() # noqa
|
||||
self.setUp()
|
||||
return wrapper(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return retry_actual
|
||||
|
@ -1,15 +1,17 @@
|
||||
apiVersion: v2
|
||||
appVersion: "0.10.0-rc1"
|
||||
description: A Helm chart for passbook.
|
||||
description: passbook is an open-source Identity Provider focused on flexibility and versatility. You can use passbook in an existing environment to add support for new protocols. passbook is also a great solution for implementing signup/recovery/etc in your application, so you don't have to deal with it.
|
||||
name: passbook
|
||||
version: "0.10.0-rc1"
|
||||
icon: https://github.com/BeryJu/passbook/blob/master/passbook/static/static/passbook/logo.svg
|
||||
home: https://passbook.beryju.org
|
||||
sources:
|
||||
- https://github.com/BeryJu/passbook
|
||||
version: "0.12.11-stable"
|
||||
icon: https://raw.githubusercontent.com/BeryJu/passbook/master/website/static/img/logo.svg
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 9.3.2
|
||||
version: 9.4.1
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
condition: install.postgresql
|
||||
- name: redis
|
||||
version: 10.7.16
|
||||
version: 10.9.0
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
condition: install.redis
|
||||
|
28
helm/README.md
Normal file
28
helm/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# passbook Helm Chart
|
||||
|
||||
| Name | Default | Description |
|
||||
|-----------------------------------|-------------------------|-------------|
|
||||
| image.name | beryju/passbook | Image used to run the passbook server and worker |
|
||||
| image.name_static | beryju/passbook-static | Image used to run the passbook static server (CSS and JS Files) |
|
||||
| image.tag | 0.12.5-stable | Image tag |
|
||||
| serverReplicas | 1 | Replicas for the Server deployment |
|
||||
| workerReplicas | 1 | Replicas for the Worker deployment |
|
||||
| kubernetesIntegration | true | Enable/disable the Kubernetes integration for passbook. This will create a service account for passbook to create and update outposts in passbook |
|
||||
| config.secretKey | | Secret key used to sign session cookies, generate with `pwgen 50 1` for example. |
|
||||
| config.errorReporting.enabled | false | Enable/disable error reporting |
|
||||
| config.errorReporting.environment | customer | Environment sent with the error reporting |
|
||||
| config.errorReporting.sendPii | false | Whether to send Personally-identifiable data with the error reporting |
|
||||
| config.logLevel | warning | Log level of passbook |
|
||||
| backup.accessKey | | Optionally enable S3 Backup, Access Key |
|
||||
| backup.secretKey | | Optionally enable S3 Backup, Secret Key |
|
||||
| backup.bucket | | Optionally enable S3 Backup, Bucket |
|
||||
| backup.region | | Optionally enable S3 Backup, Region |
|
||||
| backup.host | | Optionally enable S3 Backup, to custom Endpoint like minio |
|
||||
| ingress.annotations | {} | Annotations for the ingress object |
|
||||
| ingress.hosts | [passbook.k8s.local] | Hosts which the ingress will match |
|
||||
| ingress.tls | [] | TLS Configuration, same as Ingress objects |
|
||||
| install.postgresql | true | Enables/disables the packaged PostgreSQL Chart
|
||||
| install.redis | true | Enables/disables the packaged Redis Chart
|
||||
| postgresql.postgresqlPassword | | Password used for PostgreSQL, generated automatically.
|
||||
|
||||
For more info, see https://passbook.beryju.org/ and https://passbook.beryju.org/docs/installation/kubernetes/
|
@ -3,7 +3,7 @@
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "passbook.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- default .Chart.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
@ -12,17 +12,13 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "passbook.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- $name := default .Chart.Name -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
|
@ -7,13 +7,14 @@ data:
|
||||
POSTGRESQL__NAME: "{{ .Values.postgresql.postgresqlDatabase }}"
|
||||
POSTGRESQL__USER: "{{ .Values.postgresql.postgresqlUsername }}"
|
||||
{{- if .Values.backup }}
|
||||
POSTGRESQL__BACKUP__ACCESS_KEY: "{{ .Values.backup.access_key }}"
|
||||
POSTGRESQL__BACKUP__SECRET_KEY: "{{ .Values.backup.secret_key }}"
|
||||
POSTGRESQL__BACKUP__BUCKET: "{{ .Values.backup.bucket }}"
|
||||
POSTGRESQL__BACKUP__HOST: "{{ .Values.backup.host }}"
|
||||
POSTGRESQL__S3_BACKUP__ACCESS_KEY: "{{ .Values.backup.accessKey }}"
|
||||
POSTGRESQL__S3_BACKUP__SECRET_KEY: "{{ .Values.backup.secretKey }}"
|
||||
POSTGRESQL__S3_BACKUP__BUCKET: "{{ .Values.backup.bucket }}"
|
||||
POSTGRESQL__S3_BACKUP__REGION: "{{ .Values.backup.region }}"
|
||||
POSTGRESQL__S3_BACKUP__HOST: "{{ .Values.backup.host }}"
|
||||
{{- end}}
|
||||
REDIS__HOST: "{{ .Release.Name }}-redis-master"
|
||||
ERROR_REPORTING__ENABLED: "{{ .Values.config.error_reporting.enabled }}"
|
||||
ERROR_REPORTING__ENVIRONMENT: "{{ .Values.config.error_reporting.environment }}"
|
||||
ERROR_REPORTING__SEND_PII: "{{ .Values.config.error_reporting.send_pii }}"
|
||||
LOG_LEVEL: "{{ .Values.config.log_level }}"
|
||||
ERROR_REPORTING__ENABLED: "{{ .Values.config.errorReporting.enabled }}"
|
||||
ERROR_REPORTING__ENVIRONMENT: "{{ .Values.config.errorReporting.environment }}"
|
||||
ERROR_REPORTING__SEND_PII: "{{ .Values.config.errorReporting.sendPii }}"
|
||||
LOG_LEVEL: "{{ .Values.config.logLevel }}"
|
||||
|
@ -5,8 +5,8 @@ metadata:
|
||||
name: {{ include "passbook.fullname" . }}-secret-key
|
||||
data:
|
||||
monitoring_username: bW9uaXRvcg== # monitor in base64
|
||||
{{- if .Values.config.secret_key }}
|
||||
secret_key: {{ .Values.config.secret_key | b64enc | quote }}
|
||||
{{- if .Values.config.secretKey }}
|
||||
secret_key: {{ .Values.config.secretKey | b64enc | quote }}
|
||||
{{- else }}
|
||||
secret_key: {{ randAlphaNum 50 | b64enc | quote}}
|
||||
{{- end }}
|
||||
|
64
helm/templates/service-account.yaml
Normal file
64
helm/templates/service-account.yaml
Normal file
@ -0,0 +1,64 @@
|
||||
{{- if .Values.kubernetesIntegration }}
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: {{ include "passbook.fullname" . }}-sa-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
- services
|
||||
verbs:
|
||||
- "get"
|
||||
- "create"
|
||||
- "delete"
|
||||
- "read"
|
||||
- "patch"
|
||||
- apiGroups:
|
||||
- "extensions"
|
||||
- "apps"
|
||||
resources:
|
||||
- "deployments"
|
||||
verbs:
|
||||
- "get"
|
||||
- "create"
|
||||
- "delete"
|
||||
- "read"
|
||||
- "patch"
|
||||
- apiGroups:
|
||||
- "extensions"
|
||||
- "networking.k8s.io"
|
||||
resources:
|
||||
- "ingresses"
|
||||
verbs:
|
||||
- "get"
|
||||
- "create"
|
||||
- "delete"
|
||||
- "read"
|
||||
- "patch"
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- namespaces
|
||||
verbs:
|
||||
- list
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "passbook.fullname" . }}-sa
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: {{ include "passbook.fullname" . }}-sa-role-binding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: {{ include "passbook.fullname" . }}-sa-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "passbook.fullname" . }}-sa
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
@ -9,7 +9,7 @@ metadata:
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
k8s.passbook.beryju.org/component: web
|
||||
spec:
|
||||
replicas: {{ serverReplicas }}
|
||||
replicas: {{ .Values.serverReplicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
@ -22,10 +22,29 @@ spec:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.beryju.org/component: web
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 1
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchExpressions:
|
||||
- key: app.kubernetes.io/name
|
||||
operator: In
|
||||
values:
|
||||
- {{ include "passbook.name" . }}
|
||||
- key: app.kubernetes.io/instance
|
||||
operator: In
|
||||
values:
|
||||
- {{ .Release.Name }}
|
||||
- key: k8s.passbook.beryju.org/component
|
||||
operator: In
|
||||
values:
|
||||
- web
|
||||
topologyKey: "kubernetes.io/hostname"
|
||||
initContainers:
|
||||
- name: passbook-database-migrations
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: Always
|
||||
args: [migrate]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
@ -50,7 +69,6 @@ spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: Always
|
||||
args: [server]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
@ -82,18 +100,18 @@ spec:
|
||||
port: http
|
||||
httpHeaders:
|
||||
- name: Host
|
||||
value: kubernetes-healthcheck-host
|
||||
value: passbook-healthcheck-host
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
httpHeaders:
|
||||
- name: Host
|
||||
value: kubernetes-healthcheck-host
|
||||
value: passbook-healthcheck-host
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 200M
|
||||
memory: 300M
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 350M
|
||||
memory: 500M
|
||||
|
@ -9,7 +9,7 @@ metadata:
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
k8s.passbook.beryju.org/component: worker
|
||||
spec:
|
||||
replicas: {{ workerReplicas }}
|
||||
replicas: {{ .Values.workerReplicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
@ -22,6 +22,29 @@ spec:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.beryju.org/component: worker
|
||||
spec:
|
||||
{{- if .Values.kubernetesIntegration }}
|
||||
serviceAccountName: {{ include "passbook.fullname" . }}-sa
|
||||
{{- end }}
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 1
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchExpressions:
|
||||
- key: app.kubernetes.io/name
|
||||
operator: In
|
||||
values:
|
||||
- {{ include "passbook.name" . }}
|
||||
- key: app.kubernetes.io/instance
|
||||
operator: In
|
||||
values:
|
||||
- {{ .Release.Name }}
|
||||
- key: k8s.passbook.beryju.org/component
|
||||
operator: In
|
||||
values:
|
||||
- worker
|
||||
topologyKey: "kubernetes.io/hostname"
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
@ -50,7 +73,7 @@ spec:
|
||||
resources:
|
||||
requests:
|
||||
cpu: 150m
|
||||
memory: 300M
|
||||
memory: 400M
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 500M
|
||||
memory: 600M
|
||||
|
21
helm/values.test.yaml
Normal file
21
helm/values.test.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
image:
|
||||
tag: gh-master
|
||||
|
||||
serverReplicas: 1
|
||||
workerReplicas: 1
|
||||
|
||||
config:
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
logLevel: debug
|
||||
|
||||
ingress:
|
||||
hosts:
|
||||
- passbook.127.0.0.1.nip.io
|
||||
|
||||
# These values influence the bundled postgresql and redis charts, but are also used by passbook to connect
|
||||
postgresql:
|
||||
postgresqlPassword: EK-5jnKfjrGRm<77
|
||||
|
||||
redis:
|
||||
password: password
|
@ -4,32 +4,45 @@
|
||||
image:
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.10.0-rc1
|
||||
|
||||
nameOverride: ""
|
||||
tag: 0.12.11-stable
|
||||
|
||||
serverReplicas: 1
|
||||
workerReplicas: 1
|
||||
|
||||
# Enable the Kubernetes integration which lets passbook deploy outposts into kubernetes
|
||||
kubernetesIntegration: true
|
||||
|
||||
config:
|
||||
# Optionally specify fixed secret_key, otherwise generated automatically
|
||||
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# secretKey: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# Enable error reporting
|
||||
error_reporting:
|
||||
errorReporting:
|
||||
enabled: false
|
||||
environment: customer
|
||||
send_pii: false
|
||||
sendPii: false
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
log_level: warning
|
||||
logLevel: warning
|
||||
|
||||
# Enable Database Backups to S3
|
||||
# backup:
|
||||
# access_key: access-key
|
||||
# secret_key: secret-key
|
||||
# accessKey: access-key
|
||||
# secretKey: secret-key
|
||||
# bucket: s3-bucket
|
||||
# region: eu-central-1
|
||||
# host: s3-host
|
||||
|
||||
ingress:
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- passbook.k8s.local
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - passbook.k8s.local
|
||||
|
||||
###################################
|
||||
# Values controlling dependencies
|
||||
###################################
|
||||
@ -46,19 +59,5 @@ redis:
|
||||
cluster:
|
||||
enabled: false
|
||||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
# https://stackoverflow.com/a/59189742
|
||||
disableCommands: []
|
||||
|
||||
ingress:
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
path: /
|
||||
hosts:
|
||||
- passbook.k8s.local
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - passbook.k8s.local
|
||||
|
@ -1,14 +1,20 @@
|
||||
#!/bin/bash -e
|
||||
python -m lifecycle.wait_for_db
|
||||
printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@"
|
||||
printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@" > /dev/stderr
|
||||
if [[ "$1" == "server" ]]; then
|
||||
gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application
|
||||
elif [[ "$1" == "worker" ]]; then
|
||||
celery worker --autoscale=10,3 -E -B -A=passbook.root.celery -s=/tmp/celerybeat-schedule
|
||||
celery -A passbook.root.celery worker --autoscale 3,1 -E -B -s /tmp/celerybeat-schedule -Q passbook,passbook_scheduled
|
||||
elif [[ "$1" == "migrate" ]]; then
|
||||
# Run system migrations first, run normal migrations after
|
||||
python -m lifecycle.migrate
|
||||
python -m manage migrate
|
||||
elif [[ "$1" == "backup" ]]; then
|
||||
python -m manage dbbackup --clean
|
||||
elif [[ "$1" == "restore" ]]; then
|
||||
python -m manage dbrestore ${@:2}
|
||||
elif [[ "$1" == "bash" ]]; then
|
||||
/bin/bash
|
||||
else
|
||||
python -m manage "$@"
|
||||
fi
|
||||
|
@ -1,9 +1,12 @@
|
||||
"""Gunicorn config"""
|
||||
import os
|
||||
import warnings
|
||||
from multiprocessing import cpu_count
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
|
||||
bind = "0.0.0.0:8000"
|
||||
workers = 2
|
||||
threads = 4
|
||||
|
||||
user = "passbook"
|
||||
group = "passbook"
|
||||
@ -12,6 +15,8 @@ worker_class = "uvicorn.workers.UvicornWorker"
|
||||
# Docker containers don't have /tmp as tmpfs
|
||||
worker_tmp_dir = "/dev/shm"
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings")
|
||||
|
||||
logconfig_dict = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
@ -40,3 +45,13 @@ logconfig_dict = {
|
||||
"gunicorn": {"handlers": ["console"], "level": "INFO", "propagate": False},
|
||||
},
|
||||
}
|
||||
|
||||
# if we're running in kubernetes, use fixed workers because we can scale with more pods
|
||||
# otherwise (assume docker-compose), use as much as we can
|
||||
if Path("/var/run/secrets/kubernetes.io").exists():
|
||||
workers = 2
|
||||
else:
|
||||
worker = cpu_count() * 2 + 1
|
||||
threads = 4
|
||||
|
||||
warnings.simplefilter("once")
|
||||
|
@ -47,7 +47,9 @@ if __name__ == "__main__":
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
for _, sub in getmembers(mod, isclass):
|
||||
for name, sub in getmembers(mod, isclass):
|
||||
if name != "Migration":
|
||||
continue
|
||||
migration = sub(curr, conn)
|
||||
if migration.needs_migration():
|
||||
LOGGER.info("Migration needs to be applied", migration=sub)
|
||||
|
@ -25,7 +25,7 @@ delete from django_migrations where app = 'passbook_stages_password' and
|
||||
name = '0002_passwordstage_change_flow';"""
|
||||
|
||||
|
||||
class To010Migration(BaseMigration):
|
||||
class Migration(BaseMigration):
|
||||
def needs_migration(self) -> bool:
|
||||
self.cur.execute(
|
||||
"select * from information_schema.tables where table_name='oidc_provider_client'"
|
||||
|
@ -1,16 +1,28 @@
|
||||
#!/usr/bin/env python
|
||||
"""This file needs to be run from the root of the project to correctly
|
||||
import passbook. This is done by the dockerfile."""
|
||||
from json import dumps
|
||||
from sys import stderr
|
||||
from time import sleep
|
||||
|
||||
from psycopg2 import OperationalError, connect
|
||||
from redis import Redis
|
||||
from redis.exceptions import RedisError
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.lib.config import CONFIG
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
def j_print(event: str, log_level: str = "info", **kwargs):
|
||||
"""Print event in the same format as structlog with JSON.
|
||||
Used before structlog is configured."""
|
||||
data = {
|
||||
"event": event,
|
||||
"level": log_level,
|
||||
"logger": __name__,
|
||||
}
|
||||
data.update(**kwargs)
|
||||
print(dumps(data), file=stderr)
|
||||
|
||||
|
||||
while True:
|
||||
try:
|
||||
@ -24,7 +36,7 @@ while True:
|
||||
break
|
||||
except OperationalError:
|
||||
sleep(1)
|
||||
LOGGER.warning("PostgreSQL Connection failed, retrying...")
|
||||
j_print("PostgreSQL Connection failed, retrying...")
|
||||
|
||||
while True:
|
||||
try:
|
||||
@ -38,4 +50,4 @@ while True:
|
||||
break
|
||||
except RedisError:
|
||||
sleep(1)
|
||||
LOGGER.warning("Redis Connection failed, retrying...")
|
||||
j_print("Redis Connection failed, retrying...")
|
||||
|
78
mkdocs.yml
78
mkdocs.yml
@ -1,78 +0,0 @@
|
||||
site_name: passbook Docs
|
||||
site_url: https://passbook.beryju.org/
|
||||
copyright: "Copyright © 2019 - 2020 BeryJu.org"
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Terminology: terminology.md
|
||||
- Installation:
|
||||
- docker-compose: installation/docker-compose.md
|
||||
- Kubernetes: installation/kubernetes.md
|
||||
- Flows:
|
||||
Overview: flow/flows.md
|
||||
Examples: flow/examples/examples.md
|
||||
- Stages:
|
||||
- Captcha Stage: flow/stages/captcha/index.md
|
||||
- Dummy Stage: flow/stages/dummy/index.md
|
||||
- Email Stage: flow/stages/email/index.md
|
||||
- Identification Stage: flow/stages/identification/index.md
|
||||
- Invitation Stage: flow/stages/invitation/index.md
|
||||
- OTP Stage: flow/stages/otp/index.md
|
||||
- Password Stage: flow/stages/password/index.md
|
||||
- Prompt Stage: flow/stages/prompt/index.md
|
||||
- Prompt Stage Validation: flow/stages/prompt/validation.md
|
||||
- User Delete Stage: flow/stages/user_delete.md
|
||||
- User Login Stage: flow/stages/user_login.md
|
||||
- User Logout Stage: flow/stages/user_logout.md
|
||||
- User Write Stage: flow/stages/user_write.md
|
||||
- Sources: sources.md
|
||||
- Providers:
|
||||
- OAuth2: providers/oauth2.md
|
||||
- SAML: providers/saml.md
|
||||
- Proxy: providers/proxy.md
|
||||
- Outposts: outposts/outposts.md
|
||||
- Expressions:
|
||||
- Overview: expressions/index.md
|
||||
- Reference:
|
||||
- User Object: expressions/reference/user-object.md
|
||||
- Property Mappings:
|
||||
- Overview: property-mappings/index.md
|
||||
- Expressions: property-mappings/expression.md
|
||||
- Policies:
|
||||
- Overview: policies/index.md
|
||||
- Expression: policies/expression.md
|
||||
- Integrations:
|
||||
- as Provider:
|
||||
- Amazon Web Services: integrations/services/aws/index.md
|
||||
- GitLab: integrations/services/gitlab/index.md
|
||||
- Rancher: integrations/services/rancher/index.md
|
||||
- Harbor: integrations/services/harbor/index.md
|
||||
- Sentry: integrations/services/sentry/index.md
|
||||
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
|
||||
- Upgrading:
|
||||
- to 0.9: upgrading/to-0.9.md
|
||||
- to 0.10: upgrading/to-0.10.md
|
||||
|
||||
repo_name: "BeryJu/passbook"
|
||||
repo_url: https://github.com/BeryJu/passbook
|
||||
theme:
|
||||
name: material
|
||||
logo: images/logo.svg
|
||||
favicon: images/logo.svg
|
||||
palette:
|
||||
scheme: slate
|
||||
primary: white
|
||||
|
||||
markdown_extensions:
|
||||
- toc:
|
||||
permalink: "¶"
|
||||
- admonition
|
||||
- codehilite
|
||||
- pymdownx.betterem:
|
||||
smart_enable: all
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.magiclink
|
||||
- attr_list
|
||||
|
||||
plugins:
|
||||
- search
|
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.10.0-rc1"
|
||||
__version__ = "0.12.11-stable"
|
||||
|
0
passbook/admin/api/__init__.py
Normal file
0
passbook/admin/api/__init__.py
Normal file
80
passbook/admin/api/overview.py
Normal file
80
passbook/admin/api/overview.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""passbook administration overview"""
|
||||
from django.core.cache import cache
|
||||
from django.http import response
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from passbook import __version__
|
||||
from passbook.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
||||
from passbook.core.models import Provider
|
||||
from passbook.policies.models import Policy
|
||||
from passbook.root.celery import CELERY_APP
|
||||
|
||||
|
||||
class AdministrationOverviewSerializer(Serializer):
|
||||
"""Overview View"""
|
||||
|
||||
version = SerializerMethodField()
|
||||
version_latest = SerializerMethodField()
|
||||
worker_count = SerializerMethodField()
|
||||
providers_without_application = SerializerMethodField()
|
||||
policies_without_binding = SerializerMethodField()
|
||||
cached_policies = SerializerMethodField()
|
||||
cached_flows = SerializerMethodField()
|
||||
|
||||
def get_version(self, _) -> str:
|
||||
"""Get current version"""
|
||||
return __version__
|
||||
|
||||
def get_version_latest(self, _) -> str:
|
||||
"""Get latest version from cache"""
|
||||
version_in_cache = cache.get(VERSION_CACHE_KEY)
|
||||
if not version_in_cache:
|
||||
update_latest_version.delay()
|
||||
return __version__
|
||||
return version_in_cache
|
||||
|
||||
def get_worker_count(self, _) -> int:
|
||||
"""Ping workers"""
|
||||
return len(CELERY_APP.control.ping(timeout=0.5))
|
||||
|
||||
def get_providers_without_application(self, _) -> int:
|
||||
"""Count of providers without application"""
|
||||
return len(Provider.objects.filter(application=None))
|
||||
|
||||
def get_policies_without_binding(self, _) -> int:
|
||||
"""Count of policies not bound or use in prompt stages"""
|
||||
return len(
|
||||
Policy.objects.filter(bindings__isnull=True, promptstage__isnull=True)
|
||||
)
|
||||
|
||||
def get_cached_policies(self, _) -> int:
|
||||
"""Get cached policy count"""
|
||||
return len(cache.keys("policy_*"))
|
||||
|
||||
def get_cached_flows(self, _) -> int:
|
||||
"""Get cached flow count"""
|
||||
return len(cache.keys("flow_*"))
|
||||
|
||||
def create(self, request: Request) -> response:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AdministrationOverviewViewSet(ViewSet):
|
||||
"""Return single instance of AdministrationOverviewSerializer"""
|
||||
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
@swagger_auto_schema(responses={200: AdministrationOverviewSerializer(many=True)})
|
||||
def list(self, request: Request) -> Response:
|
||||
"""Return single instance of AdministrationOverviewSerializer"""
|
||||
serializer = AdministrationOverviewSerializer(True)
|
||||
return Response(serializer.data)
|
80
passbook/admin/api/overview_metrics.py
Normal file
80
passbook/admin/api/overview_metrics.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""passbook administration overview"""
|
||||
import time
|
||||
from collections import Counter
|
||||
from datetime import timedelta
|
||||
from typing import Dict, List
|
||||
|
||||
from django.db.models import Count, ExpressionWrapper, F
|
||||
from django.db.models.fields import DurationField
|
||||
from django.db.models.functions import ExtractHour
|
||||
from django.http import response
|
||||
from django.utils.timezone import now
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from passbook.audit.models import Event, EventAction
|
||||
|
||||
|
||||
class AdministrationMetricsSerializer(Serializer):
|
||||
"""Overview View"""
|
||||
|
||||
logins_per_1h = SerializerMethodField()
|
||||
logins_failed_per_1h = SerializerMethodField()
|
||||
|
||||
def get_events_per_1h(self, action: str) -> List[Dict[str, int]]:
|
||||
"""Get event count by hour in the last day, fill with zeros"""
|
||||
date_from = now() - timedelta(days=1)
|
||||
result = (
|
||||
Event.objects.filter(action=action, created__gte=date_from)
|
||||
.annotate(
|
||||
age=ExpressionWrapper(
|
||||
now() - F("created"), output_field=DurationField()
|
||||
)
|
||||
)
|
||||
.annotate(age_hours=ExtractHour("age"))
|
||||
.values("age_hours")
|
||||
.annotate(count=Count("pk"))
|
||||
.order_by("age_hours")
|
||||
)
|
||||
data = Counter({d["age_hours"]: d["count"] for d in result})
|
||||
results = []
|
||||
_now = now()
|
||||
for hour in range(0, -24, -1):
|
||||
results.append(
|
||||
{
|
||||
"x": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
|
||||
"y": data[hour * -1],
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
def get_logins_per_1h(self, _):
|
||||
"""Get successful logins per hour for the last 24 hours"""
|
||||
return self.get_events_per_1h(EventAction.LOGIN)
|
||||
|
||||
def get_logins_failed_per_1h(self, _):
|
||||
"""Get failed logins per hour for the last 24 hours"""
|
||||
return self.get_events_per_1h(EventAction.LOGIN_FAILED)
|
||||
|
||||
def create(self, request: Request) -> response:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AdministrationMetricsViewSet(ViewSet):
|
||||
"""Return single instance of AdministrationMetricsSerializer"""
|
||||
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
@swagger_auto_schema(responses={200: AdministrationMetricsSerializer(many=True)})
|
||||
def list(self, request: Request) -> Response:
|
||||
"""Return single instance of AdministrationMetricsSerializer"""
|
||||
serializer = AdministrationMetricsSerializer(True)
|
||||
return Response(serializer.data)
|
72
passbook/admin/api/tasks.py
Normal file
72
passbook/admin/api/tasks.py
Normal file
@ -0,0 +1,72 @@
|
||||
"""Tasks API"""
|
||||
from importlib import import_module
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http.response import Http404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, DateTimeField, IntegerField, ListField
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from passbook.lib.tasks import TaskInfo
|
||||
|
||||
|
||||
class TaskSerializer(Serializer):
|
||||
"""Serialize TaskInfo and TaskResult"""
|
||||
|
||||
task_name = CharField()
|
||||
task_description = CharField()
|
||||
task_finish_timestamp = DateTimeField(source="finish_timestamp")
|
||||
|
||||
status = IntegerField(source="result.status.value")
|
||||
messages = ListField(source="result.messages")
|
||||
|
||||
def create(self, request: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TaskViewSet(ViewSet):
|
||||
"""Read-only view set that returns all background tasks"""
|
||||
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
@swagger_auto_schema(responses={200: TaskSerializer(many=True)})
|
||||
def list(self, request: Request) -> Response:
|
||||
"""List current messages and pass into Serializer"""
|
||||
return Response(TaskSerializer(TaskInfo.all().values(), many=True).data)
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
# pylint: disable=invalid-name
|
||||
def retry(self, request: Request, pk=None) -> Response:
|
||||
"""Retry task"""
|
||||
task = TaskInfo.by_name(pk)
|
||||
if not task:
|
||||
raise Http404
|
||||
try:
|
||||
task_module = import_module(task.task_call_module)
|
||||
task_func = getattr(task_module, task.task_call_func)
|
||||
task_func.delay(*task.task_call_args, **task.task_call_kwargs)
|
||||
messages.success(
|
||||
self.request,
|
||||
_(
|
||||
"Successfully re-scheduled Task %(name)s!"
|
||||
% {"name": task.task_name}
|
||||
),
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"successful": True,
|
||||
}
|
||||
)
|
||||
except ImportError:
|
||||
# if we get an import error, the module path has probably changed
|
||||
task.delete()
|
||||
return Response({"successful": False})
|
@ -1,9 +1,35 @@
|
||||
"""YAML fields"""
|
||||
"""Additional fields"""
|
||||
import yaml
|
||||
from django import forms
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ArrayFieldSelectMultiple(forms.SelectMultiple):
|
||||
"""This is a Form Widget for use with a Postgres ArrayField. It implements
|
||||
a multi-select interface that can be given a set of `choices`.
|
||||
You can provide a `delimiter` keyword argument to specify the delimeter used.
|
||||
|
||||
https://gist.github.com/stephane/00e73c0002de52b1c601"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Accept a `delimiter` argument, and grab it (defaulting to a comma)
|
||||
self.delimiter = kwargs.pop("delimiter", ",")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
if isinstance(data, MultiValueDict):
|
||||
# Normally, we'd want a list here, which is what we get from the
|
||||
# SelectMultiple superclass, but the SimpleArrayField expects to
|
||||
# get a delimited string, so we're doing a little extra work.
|
||||
return self.delimiter.join(data.getlist(name))
|
||||
|
||||
return data.get(name)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
return super().get_context(name, value.split(self.delimiter), attrs)
|
||||
|
||||
|
||||
class CodeMirrorWidget(forms.Textarea):
|
||||
"""Custom Textarea-based Widget that triggers a CodeMirror editor"""
|
||||
|
||||
@ -15,11 +41,8 @@ class CodeMirrorWidget(forms.Textarea):
|
||||
self.mode = mode
|
||||
|
||||
def render(self, *args, **kwargs):
|
||||
if "attrs" not in kwargs:
|
||||
kwargs["attrs"] = {}
|
||||
attrs = kwargs["attrs"]
|
||||
if "class" not in attrs:
|
||||
attrs["class"] = ""
|
||||
attrs = kwargs.setdefault("attrs", {})
|
||||
attrs.setdefault("class", "")
|
||||
attrs["class"] += " codemirror"
|
||||
attrs["data-cm-mode"] = self.mode
|
||||
return super().render(*args, **kwargs)
|
||||
@ -52,10 +75,14 @@ class YAMLField(forms.JSONField):
|
||||
converted = yaml.safe_load(value)
|
||||
except yaml.YAMLError:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages["invalid"], code="invalid", params={"value": value},
|
||||
self.error_messages["invalid"],
|
||||
code="invalid",
|
||||
params={"value": value},
|
||||
)
|
||||
if isinstance(converted, str):
|
||||
return YAMLString(converted)
|
||||
if converted is None:
|
||||
return {}
|
||||
return converted
|
||||
|
||||
def bound_data(self, data, initial):
|
||||
|
@ -12,7 +12,7 @@ class UserForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
||||
model = User
|
||||
fields = ["username", "name", "email", "is_staff", "is_active", "attributes"]
|
||||
fields = ["username", "name", "email", "is_active", "attributes"]
|
||||
widgets = {
|
||||
"name": forms.TextInput,
|
||||
"attributes": CodeMirrorWidget,
|
||||
|
@ -1,26 +0,0 @@
|
||||
"""passbook admin Middleware to impersonate users"""
|
||||
|
||||
from passbook.core.models import User
|
||||
|
||||
|
||||
def impersonate(get_response):
|
||||
"""Middleware to impersonate users"""
|
||||
|
||||
def middleware(request):
|
||||
"""Middleware to impersonate users"""
|
||||
|
||||
# User is superuser and has __impersonate ID set
|
||||
if request.user.is_superuser and "__impersonate" in request.GET:
|
||||
request.session["impersonate_id"] = request.GET["__impersonate"]
|
||||
# user wants to stop impersonation
|
||||
elif "__unimpersonate" in request.GET and "impersonate_id" in request.session:
|
||||
del request.session["impersonate_id"]
|
||||
|
||||
# Actually impersonate user
|
||||
if request.user.is_superuser and "impersonate_id" in request.session:
|
||||
request.user = User.objects.get(pk=request.session["impersonate_id"])
|
||||
|
||||
response = get_response(request)
|
||||
return response
|
||||
|
||||
return middleware
|
@ -1,5 +1,10 @@
|
||||
"""passbook admin settings"""
|
||||
from celery.schedules import crontab
|
||||
|
||||
MIDDLEWARE = [
|
||||
"passbook.admin.middleware.impersonate",
|
||||
]
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"admin_latest_version": {
|
||||
"task": "passbook.admin.tasks.update_latest_version",
|
||||
"schedule": crontab(minute=0), # Run every hour
|
||||
"options": {"queue": "passbook_scheduled"},
|
||||
}
|
||||
}
|
||||
|
30
passbook/admin/tasks.py
Normal file
30
passbook/admin/tasks.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""passbook admin tasks"""
|
||||
from django.core.cache import cache
|
||||
from requests import RequestException, get
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||
from passbook.root.celery import CELERY_APP
|
||||
|
||||
LOGGER = get_logger()
|
||||
VERSION_CACHE_KEY = "passbook_latest_version"
|
||||
VERSION_CACHE_TIMEOUT = 2 * 60 * 60 # 2 hours
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def update_latest_version(self: MonitoredTask):
|
||||
"""Update latest version info"""
|
||||
try:
|
||||
data = get(
|
||||
"https://api.github.com/repos/beryju/passbook/releases/latest"
|
||||
).json()
|
||||
tag_name = data.get("tag_name")
|
||||
cache.set(VERSION_CACHE_KEY, tag_name.split("/")[1], VERSION_CACHE_TIMEOUT)
|
||||
self.set_status(
|
||||
TaskResult(
|
||||
TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"]
|
||||
)
|
||||
)
|
||||
except (RequestException, IndexError) as exc:
|
||||
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
@ -18,6 +18,7 @@
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
@ -62,18 +63,27 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<i class="pf-icon pf-icon-applications pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Applications.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any application." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no applications exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:overview' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:overview' %}">
|
||||
{% trans 'System Status' %}
|
||||
{% trans 'Overview' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
@ -46,11 +46,28 @@
|
||||
{% trans 'Providers' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:outposts' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:outposts' 'passbook_admin:outpost-create' 'passbook_admin:outpost-update' 'passbook_admin:outpost-delete' %}">
|
||||
{% trans 'Outposts' %}
|
||||
<li class="pf-c-nav__item pf-m-expanded">
|
||||
<a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Outposts' %}
|
||||
<span class="pf-c-nav__toggle">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
<section class="pf-c-nav__subnav">
|
||||
<ul class="pf-c-nav__simple-list">
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:outposts' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:outposts' 'passbook_admin:outpost-create' 'passbook_admin:outpost-update' 'passbook_admin:outpost-delete' %}">
|
||||
{% trans 'Outposts' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:outpost-service-connections' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:outpost-service-connections' 'passbook_admin:outpost-service-connections-create' 'passbook_admin:outpost-service-connections-update' 'passbook_admin:outpost-service-connections-delete' %}">
|
||||
{% trans 'Service Connections' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:property-mappings' %}"
|
||||
@ -58,7 +75,7 @@
|
||||
{% trans 'Property Mappings' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item pf-m-expandable pf-m-expanded">
|
||||
<li class="pf-c-nav__item pf-m-expanded">
|
||||
<a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Flows' %}
|
||||
<span class="pf-c-nav__toggle">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
@ -99,7 +116,7 @@
|
||||
</ul>
|
||||
</section>
|
||||
</li>
|
||||
<li class="pf-c-nav__item pf-m-expandable pf-m-expanded">
|
||||
<li class="pf-c-nav__item pf-m-expanded">
|
||||
<a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Policies' %}
|
||||
<span class="pf-c-nav__toggle">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
@ -146,6 +163,12 @@
|
||||
{% trans 'Groups' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:tasks' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:tasks' %}">
|
||||
{% trans 'System Tasks' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -18,6 +18,7 @@
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
@ -64,18 +65,27 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<i class="pf-icon pf-icon-key pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Certificates.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any certificates." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no certificates exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
|
@ -18,6 +18,7 @@
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<a href="{% url 'passbook_admin:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-secondary" type="button">{% trans 'Import' %}</a>
|
||||
@ -69,18 +70,27 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<i class="pf-icon pf-icon-process-automation pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Flows.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any flows." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no flows exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<a href="{% url 'passbook_admin:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Import' %}</a>
|
||||
|
@ -19,6 +19,7 @@
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
@ -50,7 +51,7 @@
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ group.user_set.all|length }}
|
||||
{{ group.users.all|length }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@ -61,18 +62,27 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<i class="pf-icon pf-icon-users pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Groups.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any groups." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no group exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
|
@ -3,18 +3,7 @@
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.pf-m-success {
|
||||
color: var(--pf-global--success-color--100);
|
||||
}
|
||||
.pf-m-danger {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
@ -31,8 +20,9 @@
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<a href="{% url 'passbook_admin:outpost-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
@ -43,6 +33,7 @@
|
||||
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Providers' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Health' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Version' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -50,42 +41,81 @@
|
||||
{% for outpost in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<a href="{% url 'passbook_outposts:setup' outpost_pk=outpost.pk %}">{{ outpost.name }}</a>
|
||||
<span>{{ outpost.name }}</span>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ outpost.providers.all.select_subclasses|join:", " }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
{% with health=outpost.health %}
|
||||
{% if health %}
|
||||
<i class="fas fa-check pf-m-success"></i> {{ health|naturaltime }}
|
||||
{% else %}
|
||||
<i class="fas fa-times pf-m-danger"></i> Unhealthy
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
{% with states=outpost.state %}
|
||||
{% if states|length > 0 %}
|
||||
<td role="cell">
|
||||
{% for state in states %}
|
||||
<div>
|
||||
{% if state.last_seen %}
|
||||
<i class="fas fa-check pf-m-success"></i> {{ state.last_seen|naturaltime }}
|
||||
{% else %}
|
||||
<i class="fas fa-times pf-m-danger"></i> {% trans 'Unhealthy' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td role="cell">
|
||||
{% for state in states %}
|
||||
<div>
|
||||
{% if not state.version %}
|
||||
<i class="fas fa-question-circle"></i>
|
||||
{% elif state.version_outdated %}
|
||||
<i class="fas fa-times pf-m-danger"></i> {% blocktrans with is=state.version should=state.version_should %}{{ is }}, should be {{ should }}{% endblocktrans %}
|
||||
{% else %}
|
||||
<i class="fas fa-check pf-m-success"></i> {{ state.version }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% else %}
|
||||
<td role="cell">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<td>
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:outpost-update' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:outpost-delete' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
{% get_htmls outpost as htmls %}
|
||||
{% for html in htmls %}
|
||||
{{ html|safe }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<i class="fas fa-map-marker pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Outposts.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any outposts." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no outposts exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:outpost-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
|
@ -0,0 +1,135 @@
|
||||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load passbook_utils %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="pf-icon-integration"></i>
|
||||
{% trans 'Outpost Service-Connections' %}
|
||||
</h1>
|
||||
<p>{% trans "Outpost Service-Connections define how passbook connects to external platforms to manage and deploy Outposts." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:outpost-service-connection-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Type' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Local?' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Status' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for sc in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<span>{{ sc.name }}</span>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ sc|verbose_name }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ sc.local|yesno:"Yes,No" }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{% if sc.state.healthy %}
|
||||
<i class="fas fa-check pf-m-success"></i> {{ sc.state.version }}
|
||||
{% else %}
|
||||
<i class="fas fa-times pf-m-danger"></i> {% trans 'Unhealthy' %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:outpost-service-connection-update' pk=sc.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:outpost-service-connection-delete' pk=sc.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-map-marker pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Outpost Service Connections.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any outposts." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no service connections exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:outpost-service-connection-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
@ -1,6 +1,7 @@
|
||||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
@ -10,139 +11,123 @@
|
||||
</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-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 3;grid-row-end: span 2;">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-applications"></i> {% trans 'Applications' %}
|
||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Logins over the last 24 hours' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body" style="position: relative; height:100%; width:100%">
|
||||
<canvas id="logins-last-metrics"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 2;grid-row-end: span 3;">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Apps with most usage' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ application_count }}
|
||||
</p>
|
||||
<table class="pf-c-table pf-m-compact" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Application' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Logins' %}</th>
|
||||
<th role="columnheader" scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for app in most_used_applications %}
|
||||
<tr role="row">
|
||||
<td role="cell">
|
||||
{{ app.application.name }}
|
||||
</td>
|
||||
<td role="cell">
|
||||
{{ app.total_logins }}
|
||||
</td>
|
||||
<td role="cell">
|
||||
<progress value="{{ app.total_logins }}" max="{{ most_used_applications.0.total_logins }}"></progress>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
|
||||
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:providers' %}">
|
||||
<i class="fa fa-external-link-alt"> </i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if providers_without_application.exists %}
|
||||
<p class="aggregate-status">
|
||||
<p class="pb-aggregate-card">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ provider_count }}
|
||||
</p>
|
||||
<p>{% trans 'Warning: At least one Provider has no application assigned.' %}</p>
|
||||
{% else %}
|
||||
<p class="aggregate-status">
|
||||
<p class="pb-aggregate-card">
|
||||
<i class="fa fa-check-circle"></i> {{ provider_count }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if stage_count < 1 %}
|
||||
<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 %}
|
||||
<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-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">
|
||||
<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-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
|
||||
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:policies' %}">
|
||||
<i class="fa fa-external-link-alt"> </i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if policies_without_binding %}
|
||||
<p class="aggregate-status">
|
||||
<p class="pb-aggregate-card">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ policy_count }}
|
||||
</p>
|
||||
<p>{% trans 'Policies without binding exist.' %}</p>
|
||||
{% else %}
|
||||
<p class="aggregate-status">
|
||||
<p class="pb-aggregate-card">
|
||||
<i class="fa fa-check-circle"></i> {{ policy_count }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
|
||||
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:users' %}">
|
||||
<i class="fa fa-external-link-alt"> </i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<p class="aggregate-status">
|
||||
<p class="pb-aggregate-card">
|
||||
<i class="fa fa-check-circle"></i> {{ user_count }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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 pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
|
||||
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
|
||||
</div>
|
||||
<a href="https://github.com/BeryJu/passbook/releases" target="_blank">
|
||||
<i class="fa fa-external-link-alt"> </i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<p class="aggregate-status">
|
||||
<p class="pb-aggregate-card">
|
||||
{% if version >= version_latest %}
|
||||
<i class="fa fa-check-circle"></i> {{ version }}
|
||||
{% else %}
|
||||
@ -161,97 +146,192 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
|
||||
</div>
|
||||
</div>
|
||||
<fetch-fill-slot class="pf-c-card__body" url="{% url 'passbook_api:admin_overview-list' %}" key="worker_count">
|
||||
<div slot="value < 1">
|
||||
<p class="pb-aggregate-card">
|
||||
<i class="fa fa-exclamation-triangle"></i> <span data-value></span>
|
||||
</p>
|
||||
<p>{% trans 'No workers connected.' %}</p>
|
||||
</div>
|
||||
<div slot="value >= 1">
|
||||
<p class="pb-aggregate-card">
|
||||
<i class="fa fa-check-circle"></i> <span data-value></span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</div>
|
||||
</fetch-fill-slot>
|
||||
</div>
|
||||
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
|
||||
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
|
||||
</div>
|
||||
<a data-target="modal" data-modal="clearPolicyCache">
|
||||
<i class="fa fa-trash"> </i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if worker_count < 1 %}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ worker_count }}
|
||||
{% if cached_policies < 1 %}
|
||||
<p class="pb-aggregate-card">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ cached_policies }}
|
||||
</p>
|
||||
<p>{% trans 'No workers connected.' %}</p>
|
||||
<p>{% trans 'No policies cached. Users may experience slow response times.' %}</p>
|
||||
{% else %}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ worker_count }}
|
||||
<p class="pb-aggregate-card">
|
||||
<i class="fa fa-check-circle"></i> {{ cached_policies }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if cached_policies < 1 %}
|
||||
<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 %}
|
||||
<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 pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
|
||||
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %}
|
||||
</div>
|
||||
<a data-target="modal" data-modal="clearFlowCache">
|
||||
<i class="fa fa-trash"> </i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if cached_flows < 1 %}
|
||||
<p class="aggregate-status">
|
||||
<p class="pb-aggregate-card">
|
||||
<span class="fa fa-exclamation-triangle"></span> {{ cached_flows }}
|
||||
</p>
|
||||
<p>{% trans 'No flows cached.' %}</p>
|
||||
{% else %}
|
||||
<p class="aggregate-status">
|
||||
<p class="pb-aggregate-card">
|
||||
<i class="fa fa-check-circle"></i> {{ cached_flows }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="pf-c-backdrop" id="clearCacheModalRoot" hidden>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="pf-c-backdrop" id="clearPolicyCache" hidden>
|
||||
<div class="pf-l-bullseye">
|
||||
<div class="pf-c-modal-box pf-m-sm" role="dialog">
|
||||
<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>
|
||||
<div class="pf-c-modal-box__header">
|
||||
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Cache' %}?</h1>
|
||||
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Policy Cache' %}?</h1>
|
||||
</div>
|
||||
<div class="pf-c-modal-box__body" id="modal-description">
|
||||
<form method="post" id="clearForm">
|
||||
<form method="post" id="clear_policies">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="clear">
|
||||
<input type="hidden" name="clear_policies">
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
Are you sure you want to clear the cache? This includes all user sessions and all cached Policy results.
|
||||
Are you sure you want to clear the policy cache? This will cause all policies to be re-evaluated on their next usage.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<h3>
|
||||
{% blocktrans %}
|
||||
This will also log you out.
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-modal-box__footer pf-m-align-left">
|
||||
<button form="clearForm" class="pf-c-button pf-m-primary" type="submit">{% trans 'Clear' %}</button>
|
||||
<button form="clear_policies" class="pf-c-button pf-m-primary" type="submit">{% trans 'Clear' %}</button>
|
||||
<button data-modal-close class="pf-c-button pf-m-link" type="button">{% trans 'Cancel' %}</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf-c-backdrop" id="clearFlowCache" hidden>
|
||||
<div class="pf-l-bullseye">
|
||||
<div class="pf-c-modal-box pf-m-sm" role="dialog">
|
||||
<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>
|
||||
<div class="pf-c-modal-box__header">
|
||||
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Flow Cache' %}?</h1>
|
||||
</div>
|
||||
<div class="pf-c-modal-box__body" id="modal-description">
|
||||
<form method="post" id="clear_flows">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="clear_flows">
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
Are you sure you want to clear the flow cache? This will cause all flows to be re-evaluated on their next usage.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-modal-box__footer pf-m-align-left">
|
||||
<button form="clear_flows" class="pf-c-button pf-m-primary" type="submit">{% trans 'Clear' %}</button>
|
||||
<button data-modal-close class="pf-c-button pf-m-link" type="button">{% trans 'Cancel' %}</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'node_modules/chart.js/dist/Chart.bundle.min.js' %}"></script>
|
||||
<script>
|
||||
var ctx = document.getElementById('logins-last-metrics').getContext('2d');
|
||||
fetch("{% url 'passbook_api:admin_metrics-list' %}").then(r => r.json()).then(r => {
|
||||
var myChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Failed Logins',
|
||||
backgroundColor: "rgba(201, 25, 11, .5)",
|
||||
spanGaps: true,
|
||||
data: r.logins_failed_per_1h,
|
||||
},
|
||||
{
|
||||
label: 'Successful Logins',
|
||||
backgroundColor: "rgba(189, 229, 184, .5)",
|
||||
spanGaps: true,
|
||||
data: r.logins_per_1h,
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
spanGaps: true,
|
||||
scales: {
|
||||
xAxes: [{
|
||||
stacked: true,
|
||||
gridLines: {
|
||||
color: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
type: 'time',
|
||||
offset: true,
|
||||
ticks: {
|
||||
callback: function (value, index, values) {
|
||||
const date = new Date();
|
||||
const delta = (date - values[index].value);
|
||||
const ago = Math.round(delta / 1000 / 3600);
|
||||
return `${ago} Hours ago`;
|
||||
},
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 8
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
stacked: true,
|
||||
gridLines: {
|
||||
color: "rgba(0, 0, 0, 0)",
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -18,6 +18,7 @@
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
@ -55,7 +56,7 @@
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ policy.name }}</div>
|
||||
{% if not policy.bindings.exists %}
|
||||
{% if not policy.bindings.exists and not policy.promptstage_set.exists %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i>
|
||||
<small>{% trans 'Warning: Policy is not assigned.' %}</small>
|
||||
{% else %}
|
||||
@ -78,18 +79,27 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<i class="pf-icon pf-icon-infrastructure pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Policies.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any policies." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no policies exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
|
@ -75,7 +75,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
@ -19,6 +19,7 @@
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
@ -72,18 +73,27 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<i class="pf-icon pf-icon-blueprint pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Property Mappings.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any property mappings." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no property mappings exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
|
@ -20,6 +20,7 @@
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
@ -91,18 +92,27 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<i class="pf-icon-integration pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Providers.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any providers." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no providers exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
|
@ -20,6 +20,7 @@
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
@ -85,18 +86,27 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<i class="pf-icon pf-icon-middleware pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Sources.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any sources." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no sources exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
|
@ -11,8 +11,7 @@
|
||||
<i class="pf-icon pf-icon-plugged"></i>
|
||||
{% trans 'Stages' %}
|
||||
</h1>
|
||||
<p>{% trans "Stages are single steps of a Flow that a user is guided through." %}
|
||||
</p>
|
||||
<p>{% trans "Stages are single steps of a Flow that a user is guided through." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
@ -20,6 +19,7 @@
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
@ -81,18 +81,27 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<i class="pf-icon pf-icon-plugged pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Stages.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any stages." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no stages exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
|
@ -81,7 +81,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
@ -19,6 +19,7 @@
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
@ -54,18 +55,27 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<i class="pf-icon pf-icon-migration pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Invitations.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any invitations." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no invitations exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
|
@ -19,6 +19,7 @@
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
@ -80,18 +81,27 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<i class="pf-icon pf-icon-plugged pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Stage Prompts.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any stage prompts." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no stage prompts exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
|
77
passbook/admin/templates/administration/task/list.html
Normal file
77
passbook/admin/templates/administration/task/list.html
Normal file
@ -0,0 +1,77 @@
|
||||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="pf-icon pf-icon-automation"></i>
|
||||
{% trans 'System Tasks' %}
|
||||
</h1>
|
||||
<p>{% trans "Long-running operations which passbook executes in the background." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Identifier' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Description' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Last Run' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Status' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Messages' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for task in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<pre>{{ task.task_name }}</pre>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ task.task_description }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ task.finish_timestamp|naturaltime }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{% if task.result.status == task_successful %}
|
||||
<i class="fas fa-check pf-m-success"></i> {% trans 'Successful' %}
|
||||
{% elif task.result.status == task_warning %}
|
||||
<i class="fas fa-exclamation-triangle pf-m-warning"></i> {% trans 'Warning' %}
|
||||
{% elif task.result.status == task_error %}
|
||||
<i class="fas fa-times pf-m-danger"></i> {% trans 'Error' %}
|
||||
{% else %}
|
||||
<i class="fas fa-question-circle"></i> {% trans 'Unknown' %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% for message in task.result.messages %}
|
||||
<div>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
<button is="action-button" class="pf-c-button pf-m-primary" url="{% url 'passbook_api:admin_system_tasks-retry' pk=task.task_name %}">
|
||||
{% trans 'Retry Task' %}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user