Merge branch 'master' into azure-pipelines
# Conflicts: # .github/workflows/ci.yml # README.md
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 0.8.15-beta
|
||||
current_version = 0.9.0-pre4
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
|
@ -22,4 +22,3 @@ values =
|
|||
[bumpversion:file:.github/workflows/release.yml]
|
||||
|
||||
[bumpversion:file:passbook/__init__.py]
|
||||
|
||||
|
|
2
.github/FUNDING.yml
vendored
|
@ -1 +1 @@
|
|||
custom: ["https://www.paypal.me/octocat"]
|
||||
custom: ["https://www.paypal.me/beryju"]
|
||||
|
|
15
.github/workflows/release.yml
vendored
|
@ -16,11 +16,11 @@ jobs:
|
|||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:0.8.15-beta
|
||||
-t beryju/passbook:0.9.0-pre4
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.8.15-beta
|
||||
run: docker push beryju/passbook:0.9.0-pre4
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook:latest
|
||||
build-gatekeeper:
|
||||
|
@ -37,11 +37,11 @@ jobs:
|
|||
cd gatekeeper
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/passbook-gatekeeper:0.8.15-beta \
|
||||
-t beryju/passbook-gatekeeper:0.9.0-pre4 \
|
||||
-t beryju/passbook-gatekeeper:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-gatekeeper:0.8.15-beta
|
||||
run: docker push beryju/passbook-gatekeeper:0.9.0-pre4
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-gatekeeper:latest
|
||||
build-static:
|
||||
|
@ -66,11 +66,11 @@ jobs:
|
|||
run: docker build
|
||||
--no-cache
|
||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||
-t beryju/passbook-static:0.8.15-beta
|
||||
-t beryju/passbook-static:0.9.0-pre4
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.8.15-beta
|
||||
run: docker push beryju/passbook-static:0.9.0-pre4
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-static:latest
|
||||
test-release:
|
||||
|
@ -82,8 +82,7 @@ jobs:
|
|||
- uses: actions/checkout@v1
|
||||
- name: Run test suite in final docker images
|
||||
run: |
|
||||
export PASSBOOK_DOMAIN=localhost
|
||||
docker-compose pull
|
||||
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"
|
||||
|
|
3
.github/workflows/tag.yml
vendored
|
@ -13,8 +13,7 @@ jobs:
|
|||
- uses: actions/checkout@master
|
||||
- name: Pre-release test
|
||||
run: |
|
||||
export PASSBOOK_DOMAIN=localhost
|
||||
docker-compose pull
|
||||
docker-compose pull -q
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/passbook:latest \
|
||||
|
|
3
.gitignore
vendored
|
@ -196,3 +196,6 @@ local.env.yml
|
|||
### Helm ###
|
||||
# Chart dependencies
|
||||
**/charts/*.tgz
|
||||
|
||||
# Selenium Screenshots
|
||||
selenium_screenshots/**
|
||||
|
|
4
Pipfile
|
@ -40,7 +40,7 @@ signxml = "*"
|
|||
structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
jinja2 = "*"
|
||||
facebook-sdk = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
|
@ -56,6 +56,8 @@ pylint = "*"
|
|||
pylint-django = "*"
|
||||
unittest-xml-reporting = "*"
|
||||
black = "*"
|
||||
selenium = "*"
|
||||
docker = "*"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
|
|
497
Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "c9232d99b062ee14eda43d176481f16da78ce53f912c844db3f33f1b3e8a47b7"
|
||||
"sha256": "fd0192b73c01aaffb90716ce7b6d4e5be9adb8788d3ebd58e54ccd6f85d9b71b"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -25,17 +25,10 @@
|
|||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5",
|
||||
"sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c"
|
||||
"sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
|
||||
"sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
|
||||
],
|
||||
"version": "==3.2.7"
|
||||
},
|
||||
"asn1crypto": {
|
||||
"hashes": [
|
||||
"sha256:5a215cb8dc12f892244e3a113fe05397ee23c5c4ca7a69cd6e69811755efc42d",
|
||||
"sha256:831d2710d3274c8a74befdddaf9f17fcbf6e350534565074818722d6d615b315"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
"version": "==3.2.10"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
|
@ -53,33 +46,33 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:26f8564b46d009b8f4c6470a6d6cde147b282a197339c7e31cbb0fe9fd9e5f5d",
|
||||
"sha256:f59d0bd230ed3a4b932c5c4e497a0e0ff3c93b46b7e8cde54efb6fe10c8266ba"
|
||||
"sha256:c2a223f4b48782e8b160b2130265e2a66081df111f630a5a384d6909e29a5aa9",
|
||||
"sha256:ce5a4ab6af9e993d1864209cbbb6f4812f65fbc57ad6b95e5967d8bf38b1dcfb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.13.20"
|
||||
"version": "==1.14.16"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:990f3fc33dec746829740b1a9e1fe86183cdc96aedba6a632ccfcbae03e097cc",
|
||||
"sha256:d4cc47ac989a7f1d2992ef7679fb423a7966f687becf623a291a555a2d7ce1c0"
|
||||
"sha256:99d995ef99cf77458a661f3fc64e0c3a4ce77ca30facfdf0472f44b2953dd856",
|
||||
"sha256:fe0c4f7cd6b67eff3b7cb8dff6709a65d6fca10b7b7449a493b2036915e98b4c"
|
||||
],
|
||||
"version": "==1.16.20"
|
||||
"version": "==1.17.16"
|
||||
},
|
||||
"celery": {
|
||||
"hashes": [
|
||||
"sha256:108a0bf9018a871620936c33a3ee9f6336a89f8ef0a0f567a9001f4aa361415f",
|
||||
"sha256:5b4b37e276033fe47575107a2775469f0b721646a08c96ec2c61531e4fe45f2a"
|
||||
"sha256:ef17d7dffde7fc73ecab3a3b6389d93d3213bac53fa7f28e68e33647ad50b916",
|
||||
"sha256:fd77e4248bb1b7af5f7922dd8e81156f540306e3a5c4b1c24167c1f5f06025da"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.4.2"
|
||||
"version": "==4.4.6"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304",
|
||||
"sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"
|
||||
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
|
||||
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
|
||||
],
|
||||
"version": "==2020.4.5.1"
|
||||
"version": "==2020.6.20"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
|
@ -169,11 +162,11 @@
|
|||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:051ba55d42daa3eeda3944a8e4df2bc96d4c62f94316dea217248a22563c3621",
|
||||
"sha256:9aaa6a09678e1b8f0d98a948c56482eac3e3dd2ddbfb8de70a868135ef3b5e01"
|
||||
"sha256:31a5fbbea5fc71c99e288ec0b2f00302a0a92c44b13ede80b73a6a4d6d205582",
|
||||
"sha256:5457fc953ec560c5521b41fad9e6734a4668b7ba205832191bbdff40ec61073c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.6"
|
||||
"version": "==3.0.8"
|
||||
},
|
||||
"django-cors-middleware": {
|
||||
"hashes": [
|
||||
|
@ -192,19 +185,19 @@
|
|||
},
|
||||
"django-filter": {
|
||||
"hashes": [
|
||||
"sha256:558c727bce3ffa89c4a7a0b13bc8976745d63e5fd576b3a9a851650ef11c401b",
|
||||
"sha256:c3deb57f0dd7ff94d7dce52a047516822013e2b441bed472b722a317658cfd14"
|
||||
"sha256:11e63dd759835d9ba7a763926ffb2662cf8a6dcb4c7971a95064de34dbc7e5af",
|
||||
"sha256:616848eab6fc50193a1b3730140c49b60c57a3eda1f7fc57fa8505ac156c6c75"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.2.0"
|
||||
"version": "==2.3.0"
|
||||
},
|
||||
"django-guardian": {
|
||||
"hashes": [
|
||||
"sha256:8cacf49ebcc1e545f0a8997971eec0fe109f5ed31fc2a569a7bf5615453696e2",
|
||||
"sha256:ac81e88372fdf1795d84ba065550e739b42e9c6d07cdf201cf5bbf9efa7f396c"
|
||||
"sha256:0e70706c6cda88ddaf8849bddb525b8df49de05ba0798d4b3506049f0d95cbc8",
|
||||
"sha256:ed2de26e4defb800919c5749fb1bbe370d72829fbd72895b6cf4f7f1a7607e1b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.2.0"
|
||||
"version": "==2.3.0"
|
||||
},
|
||||
"django-model-utils": {
|
||||
"hashes": [
|
||||
|
@ -231,19 +224,19 @@
|
|||
},
|
||||
"django-otp": {
|
||||
"hashes": [
|
||||
"sha256:0c67cf6f4bd6fca84027879ace9049309213b6ac81f88e954376a6b5535d96c4",
|
||||
"sha256:f456639addace8b6d1eb77f9edaada1a53dbb4d6f3c19f17c476c4e3e4beb73f"
|
||||
"sha256:97849f7bf1b50c4c36a5845ab4d2e11dd472fa8e6bcc34fe18b6d3af6e4aa449",
|
||||
"sha256:d2390e61794bc10dea2fd949cbcfb7946e9ae4fb248df5494ccc4ef9ac50427e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.9.1"
|
||||
"version": "==0.9.3"
|
||||
},
|
||||
"django-prometheus": {
|
||||
"hashes": [
|
||||
"sha256:1a8cb752ae4181e38df00e7bd7d5f6495cde18b8b3ff697c22f9d8d2fe48bf28",
|
||||
"sha256:9f024af5495447c8e309f07e5289e7bc1100c5a380ac7cd0afe3a1b2a0b3b534"
|
||||
"sha256:054924b6aedd41f3f76940578127e7fac852a696805f956da39b7941c78bba91",
|
||||
"sha256:208e55e3a11ac9edd53a3b342b033e140ffe27914af1202f0b064edf99e83fc3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.1.0.dev14"
|
||||
"version": "==2.1.0.dev46"
|
||||
},
|
||||
"django-recaptcha": {
|
||||
"hashes": [
|
||||
|
@ -313,6 +306,14 @@
|
|||
],
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"facebook-sdk": {
|
||||
"hashes": [
|
||||
"sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e",
|
||||
"sha256:cabcd2e69ea3d9f042919c99b353df7aa1e2be86d040121f6e9f5e63c1cf0f8d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"future": {
|
||||
"hashes": [
|
||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||
|
@ -321,17 +322,17 @@
|
|||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
|
||||
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"version": "==2.9"
|
||||
"version": "==2.10"
|
||||
},
|
||||
"inflection": {
|
||||
"hashes": [
|
||||
"sha256:32a5c3341d9583ec319548b9015b7fbdf8c429cbcb575d326c33ae3a0e90d52c",
|
||||
"sha256:9a15d3598f01220e93f2207c432cfede50daff53137ce660fb8be838ef1ca6cc"
|
||||
"sha256:88b101b2668a1d81d6d72d4c2018e53bc6c7fc544c987849da1c7f77545c3bc9",
|
||||
"sha256:f576e85132d34f5bf7df5183c2c6f94cfb32e528f53065345cf71329ba0b8924"
|
||||
],
|
||||
"version": "==0.4.0"
|
||||
"version": "==0.5.0"
|
||||
},
|
||||
"itypes": {
|
||||
"hashes": [
|
||||
|
@ -345,7 +346,6 @@
|
|||
"sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484",
|
||||
"sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.0a1"
|
||||
},
|
||||
"jmespath": {
|
||||
|
@ -364,11 +364,11 @@
|
|||
},
|
||||
"kombu": {
|
||||
"hashes": [
|
||||
"sha256:ab0afaa5388dd2979cbc439d3623b86a4f7a58d41f621096bef7767c37bc2505",
|
||||
"sha256:aece08f48706743aaa1b9d607fee300559481eafcc5ee56451aa0ef867a3be07"
|
||||
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
|
||||
"sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.6.9"
|
||||
"version": "==4.6.11"
|
||||
},
|
||||
"ldap3": {
|
||||
"hashes": [
|
||||
|
@ -520,74 +520,74 @@
|
|||
},
|
||||
"pycryptodome": {
|
||||
"hashes": [
|
||||
"sha256:07024fc364869eae8d6ac0d316e089956e6aeffe42dbdcf44fe1320d96becf7f",
|
||||
"sha256:09b6d6bcc01a4eb1a2b4deeff5aa602a108ec5aed8ac75ae554f97d1d7f0a5ad",
|
||||
"sha256:0e10f352ccbbcb5bb2dc4ecaf106564e65702a717d72ab260f9ac4c19753cfc2",
|
||||
"sha256:1f4752186298caf2e9ff5354f2e694d607ca7342aa313a62005235d46e28cf04",
|
||||
"sha256:2fbc472e0b567318fe2052281d5a8c0ae70099b446679815f655e9fbc18c3a65",
|
||||
"sha256:3ec3dc2f80f71fd0c955ce48b81bfaf8914c6f63a41a738f28885a1c4892968a",
|
||||
"sha256:426c188c83c10df71f053e04b4003b1437bae5cb37606440e498b00f160d71d0",
|
||||
"sha256:626c0a1d4d83ec6303f970a17158114f75c3ba1736f7f2983f7b40a265861bd8",
|
||||
"sha256:767ad0fb5d23efc36a4d5c2fc608ac603f3de028909bcf59abc943e0d0bc5a36",
|
||||
"sha256:7ac729d9091ed5478af2b4a4f44f5335a98febbc008af619e4569a59fe503e40",
|
||||
"sha256:83295a3fb5cf50c48631eb5b440cb5e9832d8c14d81d1d45f4497b67a9987de8",
|
||||
"sha256:8be56bde3312e022d9d1d6afa124556460ad5c844c2fc63642f6af723c098d35",
|
||||
"sha256:8f06556a8f7ea7b1e42eff39726bb0dca1c251205debae64e6eebea3cd7b438a",
|
||||
"sha256:9230fcb5d948c3fb40049bace4d33c5d254f8232c2c0bba05d2570aea3ba4520",
|
||||
"sha256:9378c309aec1f8cd8bad361ed0816a440151b97a2a3f6ffdaba1d1a1fb76873a",
|
||||
"sha256:9977086e0f93adb326379897437373871b80501e1d176fec63c7f46fb300c862",
|
||||
"sha256:9a94fca11fdc161460bd8659c15b6adef45c1b20da86402256eaf3addfaab324",
|
||||
"sha256:9c739b7795ccf2ef1fdad8d44e539a39ad300ee6786e804ea7f0c6a786eb5343",
|
||||
"sha256:b1e332587b3b195542e77681389c296e1837ca01240399d88803a075447d3557",
|
||||
"sha256:c109a26a21f21f695d369ff9b87f5d43e0d6c768d8384e10bc74142bed2e092e",
|
||||
"sha256:c818dc1f3eace93ee50c2b6b5c2becf7c418fa5dd1ba6fc0ef7db279ea21d5e4",
|
||||
"sha256:cff31f5a8977534f255f729d5d2467526f2b10563a30bbdade92223e0bf264bd",
|
||||
"sha256:d4f94368ce2d65873a87ad867eb3bf63f4ba81eb97a9ee66d38c2b71ce5a7439",
|
||||
"sha256:d61b012baa8c2b659e9890011358455c0019a4108536b811602d2f638c40802a",
|
||||
"sha256:d6e1bc5c94873bec742afe2dfadce0d20445b18e75c47afc0c115b19e5dd38dd",
|
||||
"sha256:ea83bcd9d6c03248ebd46e71ac313858e0afd5aa2fa81478c0e653242f3eb476",
|
||||
"sha256:ed5761b37615a1f222c5345bbf45272ae2cf8c7dff88a4f53a1e9f977cbb6d95",
|
||||
"sha256:f011cd0062e54658b7086a76f8cf0f4222812acc66e219e196ea2d0a8849d0ed",
|
||||
"sha256:f1add21b6d179179b3c177c33d18a2186a09cc0d3af41ff5ed3f377360b869f2",
|
||||
"sha256:f655addaaaa9974108d4808f4150652589cada96074c87115c52e575bfcd87d5"
|
||||
"sha256:02e51e1d5828d58f154896ddfd003e2e7584869c275e5acbe290443575370fba",
|
||||
"sha256:03d5cca8618620f45fd40f827423f82b86b3a202c8d44108601b0f5f56b04299",
|
||||
"sha256:0e24171cf01021bc5dc17d6a9d4f33a048f09d62cc3f62541e95ef104588bda4",
|
||||
"sha256:132a56abba24e2e06a479d8e5db7a48271a73a215f605017bbd476d31f8e71c1",
|
||||
"sha256:1e655746f539421d923fd48df8f6f40b3443d80b75532501c0085b64afed9df5",
|
||||
"sha256:2b998dc45ef5f4e5cf5248a6edfcd8d8e9fb5e35df8e4259b13a1b10eda7b16b",
|
||||
"sha256:360955eece2cd0fa694a708d10303c6abd7b39614fa2547b6bd245da76198beb",
|
||||
"sha256:39ef9fb52d6ec7728fce1f1693cb99d60ce302aeebd59bcedea70ca3203fda60",
|
||||
"sha256:4350a42028240c344ee855f032c7d4ad6ff4f813bfbe7121547b7dc579ecc876",
|
||||
"sha256:50348edd283afdccddc0938cdc674484533912ba8a99a27c7bfebb75030aa856",
|
||||
"sha256:54bdedd28476dea8a3cd86cb67c0df1f0e3d71cae8022354b0f879c41a3d27b2",
|
||||
"sha256:55eb61aca2c883db770999f50d091ff7c14016f2769ad7bca3d9b75d1d7c1b68",
|
||||
"sha256:6276478ada411aca97c0d5104916354b3d740d368407912722bd4d11aa9ee4c2",
|
||||
"sha256:67dcad1b8b201308586a8ca2ffe89df1e4f731d5a4cdd0610cc4ea790351c739",
|
||||
"sha256:709b9f144d23e290b9863121d1ace14a72e01f66ea9c903fbdc690520dfdfcf0",
|
||||
"sha256:8063a712fba642f78d3c506b0896846601b6de7f5c3d534e388ad0cc07f5a149",
|
||||
"sha256:80d57177a0b7c14d4594c62bbb47fe2f6309ad3b0a34348a291d570925c97a82",
|
||||
"sha256:a207231a52426de3ff20f5608f0687261a3329d97a036c51f7d4c606a6f30c23",
|
||||
"sha256:abc2e126c9490e58a36a0f83516479e781d83adfb134576a5cbe5c6af2a3e93c",
|
||||
"sha256:b56638d58a3a4be13229c6a815cd448f9e3ce40c00880a5398471b42ee86f50e",
|
||||
"sha256:bcd5b8416e73e4b0d48afba3704d8c826414764dafaed7a1a93c442188d90ccc",
|
||||
"sha256:bec2bcdf7c9ce7f04d718e51887f3b05dc5c1cfaf5d2c2e9065ecddd1b2f6c9a",
|
||||
"sha256:c8bf40cf6e281a4378e25846924327e728a887e8bf0ee83b2604a0f4b61692e8",
|
||||
"sha256:d8074c8448cfd0705dfa71ca333277fce9786d0b9cac75d120545de6253f996a",
|
||||
"sha256:dd302b6ae3965afeb5ef1b0d92486f986c0e65183cd7835973f0b593800590e6",
|
||||
"sha256:de6e1cd75677423ff64712c337521e62e3a7a4fc84caabbd93207752e831a85a",
|
||||
"sha256:ef39c98d9b8c0736d91937d193653e47c3b19ddf4fc3bccdc5e09aaa4b0c5d21",
|
||||
"sha256:f521178e5a991ffd04182ed08f552daca1affcb826aeda0e1945cd989a9d4345",
|
||||
"sha256:f78a68c2c820e4731e510a2df3eef0322f24fde1781ced970bf497b6c7d92982",
|
||||
"sha256:fbe65d5cfe04ff2f7684160d50f5118bdefb01e3af4718eeb618bfed40f19d94"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.9.7"
|
||||
"version": "==3.9.8"
|
||||
},
|
||||
"pycryptodomex": {
|
||||
"hashes": [
|
||||
"sha256:1537d2d15b604b303aef56e7f440895a1c81adbee786b91f1f06eddc34da5314",
|
||||
"sha256:1d20ab8369b7558168fc014a0745c678613f9f486dae468cca2d68145196b8a4",
|
||||
"sha256:1ecc9db7409db67765eb008e558879d298406642d33ade43a6488224d23e8081",
|
||||
"sha256:37033976f72af829fe15f7fe5fe1dbed308cc43a98d9dd9d2a0a76de8ca5ee78",
|
||||
"sha256:3c3dd9d4c9c1e279d3945ae422895c901f98987333acc132dc094faf52afec35",
|
||||
"sha256:3c9b3fba037ea52c626060c5a87ee6de7e86c99e8a7c6ee07302539985d2bd64",
|
||||
"sha256:45ee555fc5e28c119a46d44ce373f5237e54a35c61b750fb3a94446b09855dbc",
|
||||
"sha256:4c93038ac011b36512cb0bf2ee3e2aec774e8bc81021d015917c89fe02bb0ee5",
|
||||
"sha256:50163324834edd0c9ce3e4512ded3e221c969086e10fdd5d3fdcaadac5e24a78",
|
||||
"sha256:59b0ea9cda5490f924771456912a225d8d9e678891f9f986661af718534719b2",
|
||||
"sha256:5cf306a17cccc327a33cdc3845629fa13f4573a4ec620ed607c79cf6785f2e27",
|
||||
"sha256:5fff8da399af16a1855f58771223acbbdac720b9969cd03fc5013d2e9a7bd9a4",
|
||||
"sha256:68650ce5b9f7152b8283302a4617269f821695a612692640dd247bd12ab21c0b",
|
||||
"sha256:6b3a9a562688996f760b5077714c3ab8b62ca56061b6e9ab7906841e43e19f91",
|
||||
"sha256:7e938ed51a59e29431ea86fab60423ada2757728db0f78952329fa02a789bd31",
|
||||
"sha256:87aa70daad6f039e814790a06422a3189311198b674b62f13933a2bdcb6b1bcc",
|
||||
"sha256:99be3a1df2b2b9f731ebe1c264a2c07c465e71cee68e35e1640b645b5213a755",
|
||||
"sha256:a3f2908666e6f74b8c4893f86dd02e16170f50e4a78ae7f3468b6208d54bc205",
|
||||
"sha256:ae3d44a639fd11dbdeca47e35e94febb1ee8bc15daf26673331add37146e0b85",
|
||||
"sha256:afb4c2fa3c6f492fd9a8b38d76e13f32d429b8e5e1e00238309391b5591cde0d",
|
||||
"sha256:b1515ce3a8a2c3fa537d137c5ca5f8b7a902044d04e07d7c3aa26c3e026120fb",
|
||||
"sha256:bf391b377413a197000b43ef2b74359974d8927d329a897c9f5ba7b63dca7b9c",
|
||||
"sha256:c436919117c23355740c669f89720673578b9aa4569bbfe105f6c10101fc1966",
|
||||
"sha256:d2c3c280975638e2a2c2fd9cb36ab111980219757fa163a2755594b9448e4138",
|
||||
"sha256:e585d530764c459cbd5d460aed0288807bb881f376ca9a20e653645217895961",
|
||||
"sha256:e76e6638ead4a7d93262a24218f0ff3ff74de6b6c823b7e19dccb31b6a481978",
|
||||
"sha256:ebfc2f885cafda076c31ae30fa0dd81e7e919ec34059a88d3018ed66e83fcce3",
|
||||
"sha256:f5797a39933a3d41526da60856735e6684b2b71a8ca99d5f79555ca121be2f4b",
|
||||
"sha256:f7e5fc5e124200b19a14be33fb0099e956e6ebb5e25d287b0829ef0a78ed76c7",
|
||||
"sha256:fb350e31e55211fec8ddc89fc0256f3b9bc3b44b68a8bde1cf44b3b4e80c0e42"
|
||||
"sha256:06f5a458624c9b0e04c0086c7f84bcc578567dab0ddc816e0476b3057b18339f",
|
||||
"sha256:1714675fb4ac29a26ced38ca22eb8ffd923ac851b7a6140563863194d7158422",
|
||||
"sha256:17272d06e4b2f6455ee2cbe93e8eb50d9450a5dc6223d06862ee1ea5d1235861",
|
||||
"sha256:2199708ebeed4b82eb45b10e1754292677f5a0df7d627ee91ea01290b9bab7e6",
|
||||
"sha256:2275a663c9e744ee4eace816ef2d446b3060554c5773a92fbc79b05bf47debda",
|
||||
"sha256:2710fc8d83b3352b370db932b3710033b9d630b970ff5aaa3e7458b5336e3b32",
|
||||
"sha256:35b9c9177a9fe7288b19dd41554c9c8ca1063deb426dd5a02e7e2a7416b6bd11",
|
||||
"sha256:3caa32cf807422adf33c10c88c22e9e2e08b9d9d042f12e1e25fe23113dd618f",
|
||||
"sha256:48cc2cfc251f04a6142badeb666d1ff49ca6fdfc303fd72579f62b768aaa52b9",
|
||||
"sha256:4ae6379350a09339109e9b6f419bb2c3f03d3e441f4b0f5b8ca699d47cc9ff7e",
|
||||
"sha256:4e0b27697fa1621c6d3d3b4edeec723c2e841285de6a8d378c1962da77b349be",
|
||||
"sha256:58e19560814dabf5d788b95a13f6b98279cf41a49b1e49ee6cf6c79a57adb4c9",
|
||||
"sha256:8044eae59301dd392fbb4a7c5d64e1aea8ef0be2540549807ecbe703d6233d68",
|
||||
"sha256:89be1bf55e50116fe7e493a7c0c483099770dd7f81b87ac8d04a43b1a203e259",
|
||||
"sha256:8fcdda24dddf47f716400d54fc7f75cadaaba1dd47cc127e59d752c9c0fc3c48",
|
||||
"sha256:914fbb18e29c54585e6aa39d300385f90d0fa3b3cc02ed829b08f95c1acf60c2",
|
||||
"sha256:93a75d1acd54efed314b82c952b39eac96ce98d241ad7431547442e5c56138aa",
|
||||
"sha256:9fd758e5e2fe02d57860b85da34a1a1e7037155c4eadc2326fc7af02f9cae214",
|
||||
"sha256:a2bc4e1a2e6ca3a18b2e0be6131a23af76fecb37990c159df6edc7da6df913e3",
|
||||
"sha256:a2ee8ba99d33e1a434fcd27d7d0aa7964163efeee0730fe2efc9d60edae1fc71",
|
||||
"sha256:b2d756620078570d3f940c84bc94dd30aa362b795cce8b2723300a8800b87f1c",
|
||||
"sha256:c0d085c8187a1e4d3402f626c9e438b5861151ab132d8761d9c5ce6491a87761",
|
||||
"sha256:c990f2c58f7c67688e9e86e6557ed05952669ff6f1343e77b459007d85f7df00",
|
||||
"sha256:ccbbec59bf4b74226170c54476da5780c9176bae084878fc94d9a2c841218e34",
|
||||
"sha256:dc2bed32c7b138f1331794e454a953360c8cedf3ee62ae31f063822da6007489",
|
||||
"sha256:e070a1f91202ed34c396be5ea842b886f6fa2b90d2db437dc9fb35a26c80c060",
|
||||
"sha256:e42860fbe1292668b682f6dabd225fbe2a7a4fa1632f0c39881c019e93dea594",
|
||||
"sha256:e4e1c486bf226822c8dceac81d0ec59c0a2399dbd1b9e04f03c3efa3605db677",
|
||||
"sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46",
|
||||
"sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb"
|
||||
],
|
||||
"version": "==3.9.7"
|
||||
"version": "==3.9.8"
|
||||
},
|
||||
"pyjwkest": {
|
||||
"hashes": [
|
||||
|
@ -604,10 +604,10 @@
|
|||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492",
|
||||
"sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a"
|
||||
"sha256:1060635ca5ac864c2b7bc7b05a448df4e32d7d8c65e33cbe1514810d339672a2",
|
||||
"sha256:56a551039101858c9e189ac9e66e330a03fb7079e97ba6b50193643905f450ce"
|
||||
],
|
||||
"version": "==3.0.0a1"
|
||||
"version": "==3.0.0a2"
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
|
@ -631,35 +631,21 @@
|
|||
},
|
||||
"pyuwsgi": {
|
||||
"hashes": [
|
||||
"sha256:15a4626740753b0d0dfeeac7d367f9b2e89ab6af16c195927e60f75359fc1bbc",
|
||||
"sha256:24c40c3b889eb9f283d43feffbc0f7c7fc024e914451425156ddb68af3df1e71",
|
||||
"sha256:393737bd43a7e38f0a4a1601a37a69c4bf893635b37665ff958170fdb604fdb7",
|
||||
"sha256:5a08308f87e639573c1efaa5966a6d04410cd45a73c4586a932fe3ee4b56369d",
|
||||
"sha256:5f4b36c0dbb9931c4da8008aa423158be596e3b4a23cec95a958631603a94e45",
|
||||
"sha256:7c31794f71bbd0ccf542cab6bddf38aa69e84e31ae0f9657a2e18ebdc150c01a",
|
||||
"sha256:802ec6dad4b6707b934370926ec1866603abe31ba03c472f56149001b3533ba1",
|
||||
"sha256:814d73d4569add69a6c19bb4a27cd5adb72b196e5e080caed17dbda740402072",
|
||||
"sha256:829299cd117cf8abe837796bf587e61ce6bfe18423a3a1c510c21e9825789c2c",
|
||||
"sha256:85f2210ceae5f48b7d8fad2240d831f4b890cac85cd98ca82683ac6aa481dfc8",
|
||||
"sha256:861c94442b28cd64af033e88e0f63c66dbd5609f67952dc18694098b47a43f3a",
|
||||
"sha256:957bc6316ffc8463795d56d9953d58e7f32aa5aad1c5ac80bc45c69f3299961e",
|
||||
"sha256:9760c3f56fb5f15852d163429096600906478e9ed2c189a52f2bb21d8a2a986c",
|
||||
"sha256:9fdfb98a2992de01e8efad2aeed22c825e36db628b144b2d6b93d81fb549f811",
|
||||
"sha256:a4b24703ea818196d0be1dc64b3b57b79c67e8dee0cfa207a4216220912035a7",
|
||||
"sha256:ad7f4968c1ddbf139a306d9b075360d959cc554d994ba5e1f512af9a40e62357",
|
||||
"sha256:b1127d34b90f74faf1707718c57a4193ac028b9f4aec0238638983132297d456",
|
||||
"sha256:bcb04d6ec644b3e08d03c64851e06edd7110489261e50627a4bcadf66ff6920e",
|
||||
"sha256:bebfebb9ee83d7cf37668bf54275b677b7ae283e84a944f9f3ac6a4b66f95d4b",
|
||||
"sha256:c29892dafc65a8b6eb95823fa4bac7754ca3fd1c28ab8d2a973289531b340a27",
|
||||
"sha256:cb296b50b51ba022b0090b28d032ff1dd395a6db03672b65a39e83532edad527",
|
||||
"sha256:ce777ebdf49ce736fc04abf555b5c41ab3f130127543a689dcf8d4871cd18fe4",
|
||||
"sha256:d8b4bf930b6a19bc9ee982b9163d948c87501ad91b71516924e8ed25fe85d2ee",
|
||||
"sha256:e2a420f2c4d35f3ec0b7e752a80d7bd385e2c5a64f67c05f2d2d74230e3114b6",
|
||||
"sha256:ef5eb630f541af6b69378d58594be90a0922fa6d6a50a9248c25b9502585f6bf",
|
||||
"sha256:fed899ce96f4f2b4d1b9f338dd145a4040ee1d8a5152213af0dd8d4a4d36e9fe"
|
||||
"sha256:1a4dd8d99b8497f109755e09484b0bd2aeaa533f7621e7c7e2a120a72111219d",
|
||||
"sha256:206937deaebbac5c87692657c3151a5a9d40ecbc9b051b94154205c50a48e963",
|
||||
"sha256:2cf35d9145208cc7c96464d688caa3de745bfc969e1a1ae23cb046fc10b0ac7e",
|
||||
"sha256:3ab84a168633eeb55847d59475d86e9078d913d190c2a1aed804c562a10301a3",
|
||||
"sha256:430406d1bcf288a87f14fde51c66877eaf5e98516838a1c6f761af5d814936fc",
|
||||
"sha256:72be25ce7aa86c5616c59d12c2961b938e7bde47b7ff6a996ff83b89f7c5cd27",
|
||||
"sha256:aa4d615de430e2066a1c76d9cc2a70abf2dfc703a82c21aee625b445866f2c3b",
|
||||
"sha256:aadd231256a672cf4342ef9fb976051949e4d5b616195e696bcb7b8a9c07789e",
|
||||
"sha256:b15ee6a7759b0465786d856334b8231d882deda5291cf243be6a343a8f3ef910",
|
||||
"sha256:bd1d0a8d4cb87eb63417a72e6b1bac47053f9b0be550adc6d2a375f4cbaa22f0",
|
||||
"sha256:d5787779ec24b67ac8898be9dc2b2b4e35f17d79f14361f6cf303d6283a848f2",
|
||||
"sha256:ecfae85d6504e0ecbba100a795032a88ce8f110b62b93243f2df1bd116eca67f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.18.post0"
|
||||
"version": "==2.0.19.1"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
|
@ -695,10 +681,10 @@
|
|||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
|
||||
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
|
||||
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
|
||||
],
|
||||
"version": "==2.23.0"
|
||||
"version": "==2.24.0"
|
||||
},
|
||||
"requests-oauthlib": {
|
||||
"hashes": [
|
||||
|
@ -749,11 +735,11 @@
|
|||
},
|
||||
"sentry-sdk": {
|
||||
"hashes": [
|
||||
"sha256:0e5e947d0f7a969314aa23669a94a9712be5a688ff069ff7b9fc36c66adc160c",
|
||||
"sha256:799a8bf76b012e3030a881be00e97bc0b922ce35dde699c6537122b751d80e2c"
|
||||
"sha256:06825c15a78934e78941ea25910db71314c891608a46492fc32c15902c6b2119",
|
||||
"sha256:3ac0c430761b3cb7682ce612151d829f8644bb3830d4e530c75b02ceb745ff49"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.14.4"
|
||||
"version": "==0.15.1"
|
||||
},
|
||||
"service-identity": {
|
||||
"hashes": [
|
||||
|
@ -765,11 +751,11 @@
|
|||
},
|
||||
"signxml": {
|
||||
"hashes": [
|
||||
"sha256:2e186c117284fe5a0c543f5bcdde68f5a2341eeae219af9eb7e512dacf4bfce7",
|
||||
"sha256:7d6af724542cae915bbb9000d333a52ce495d0b3cdcb4dc590c3c4a149b079ed"
|
||||
"sha256:4c996153153c9b1eb7ff40cf624722946f8c2ab059febfa641e54cd59725acd9",
|
||||
"sha256:d116c283f2c940bc2b4edf011330107ba02f197650a4878466987e04142d43b1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.7.2"
|
||||
"version": "==2.8.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
|
@ -795,10 +781,11 @@
|
|||
},
|
||||
"swagger-spec-validator": {
|
||||
"hashes": [
|
||||
"sha256:ba4e636d89bf547a6f41a6945bb503cf06dde13664ad978a7407aaed772ae75f"
|
||||
"sha256:d1514ec7e3c058c701f27cc74f85ceb876d6418c9db57786b9c54085ed5e29eb",
|
||||
"sha256:f4f23ee4dbd52bfcde90b1144dde22304add6260e9f29252e9fd7814c9b8fd16"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.6.0"
|
||||
"version": "==2.7.3"
|
||||
},
|
||||
"uritemplate": {
|
||||
"hashes": [
|
||||
|
@ -837,17 +824,17 @@
|
|||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5",
|
||||
"sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c"
|
||||
"sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
|
||||
"sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
|
||||
],
|
||||
"version": "==3.2.7"
|
||||
"version": "==3.2.10"
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1",
|
||||
"sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"
|
||||
"sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703",
|
||||
"sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"
|
||||
],
|
||||
"version": "==2.4.1"
|
||||
"version": "==2.4.2"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
|
@ -894,6 +881,53 @@
|
|||
"index": "pypi",
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
|
||||
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
|
||||
],
|
||||
"version": "==2020.6.20"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
|
||||
"sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
|
||||
"sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
|
||||
"sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
|
||||
"sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
|
||||
"sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
|
||||
"sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
|
||||
"sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
|
||||
"sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
|
||||
"sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
|
||||
"sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
|
||||
"sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
|
||||
"sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
|
||||
"sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
|
||||
"sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
|
||||
"sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
|
||||
"sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
|
||||
"sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
|
||||
"sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
|
||||
"sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
|
||||
"sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
|
||||
"sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
|
||||
"sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
|
||||
"sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
|
||||
"sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
|
||||
"sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
|
||||
"sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
|
||||
"sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
|
@ -946,13 +980,37 @@
|
|||
"index": "pypi",
|
||||
"version": "==5.1"
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6",
|
||||
"sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b",
|
||||
"sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5",
|
||||
"sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf",
|
||||
"sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e",
|
||||
"sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b",
|
||||
"sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae",
|
||||
"sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b",
|
||||
"sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0",
|
||||
"sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b",
|
||||
"sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d",
|
||||
"sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229",
|
||||
"sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3",
|
||||
"sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365",
|
||||
"sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55",
|
||||
"sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270",
|
||||
"sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e",
|
||||
"sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785",
|
||||
"sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0"
|
||||
],
|
||||
"version": "==2.9.2"
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:051ba55d42daa3eeda3944a8e4df2bc96d4c62f94316dea217248a22563c3621",
|
||||
"sha256:9aaa6a09678e1b8f0d98a948c56482eac3e3dd2ddbfb8de70a868135ef3b5e01"
|
||||
"sha256:31a5fbbea5fc71c99e288ec0b2f00302a0a92c44b13ede80b73a6a4d6d205582",
|
||||
"sha256:5457fc953ec560c5521b41fad9e6734a4668b7ba205832191bbdff40ec61073c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.6"
|
||||
"version": "==3.0.8"
|
||||
},
|
||||
"django-debug-toolbar": {
|
||||
"hashes": [
|
||||
|
@ -962,6 +1020,14 @@
|
|||
"index": "pypi",
|
||||
"version": "==2.2"
|
||||
},
|
||||
"docker": {
|
||||
"hashes": [
|
||||
"sha256:03a46400c4080cb6f7aa997f881ddd84fef855499ece219d75fbdb53289c17ab",
|
||||
"sha256:26eebadce7e298f55b76a88c4f8802476c5eaddbdbe38dbc6cce8781c47c9b54"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.2.2"
|
||||
},
|
||||
"gitdb": {
|
||||
"hashes": [
|
||||
"sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac",
|
||||
|
@ -976,6 +1042,13 @@
|
|||
],
|
||||
"version": "==3.1.3"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
|
||||
|
@ -1037,13 +1110,20 @@
|
|||
],
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
|
||||
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
|
||||
],
|
||||
"version": "==2.20"
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c",
|
||||
"sha256:dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b"
|
||||
"sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc",
|
||||
"sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.5.2"
|
||||
"version": "==2.5.3"
|
||||
},
|
||||
"pylint-django": {
|
||||
"hashes": [
|
||||
|
@ -1060,6 +1140,13 @@
|
|||
],
|
||||
"version": "==0.6"
|
||||
},
|
||||
"pyopenssl": {
|
||||
"hashes": [
|
||||
"sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504",
|
||||
"sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507"
|
||||
],
|
||||
"version": "==19.1.0"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
|
||||
|
@ -1086,29 +1173,44 @@
|
|||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:1386e75c9d1574f6aa2e4eb5355374c8e55f9aac97e224a8a5a6abded0f9c927",
|
||||
"sha256:27ff7325b297fb6e5ebb70d10437592433601c423f5acf86e5bc1ee2919b9561",
|
||||
"sha256:329ba35d711e3428db6b45a53b1b13a0a8ba07cbbcf10bbed291a7da45f106c3",
|
||||
"sha256:3a9394197664e35566242686d84dfd264c07b20f93514e2e09d3c2b3ffdf78fe",
|
||||
"sha256:51f17abbe973c7673a61863516bdc9c0ef467407a940f39501e786a07406699c",
|
||||
"sha256:579ea215c81d18da550b62ff97ee187b99f1b135fd894a13451e00986a080cad",
|
||||
"sha256:70c14743320a68c5dac7fc5a0f685be63bc2024b062fe2aaccc4acc3d01b14a1",
|
||||
"sha256:7e61be8a2900897803c293247ef87366d5df86bf701083b6c43119c7c6c99108",
|
||||
"sha256:8044d1c085d49673aadb3d7dc20ef5cb5b030c7a4fa253a593dda2eab3059929",
|
||||
"sha256:89d76ce33d3266173f5be80bd4efcbd5196cafc34100fdab814f9b228dee0fa4",
|
||||
"sha256:99568f00f7bf820c620f01721485cad230f3fb28f57d8fbf4a7967ec2e446994",
|
||||
"sha256:a7c37f048ec3920783abab99f8f4036561a174f1314302ccfa4e9ad31cb00eb4",
|
||||
"sha256:c2062c7d470751b648f1cacc3f54460aebfc261285f14bc6da49c6943bd48bdd",
|
||||
"sha256:c9bce6e006fbe771a02bda468ec40ffccbf954803b470a0345ad39c603402577",
|
||||
"sha256:ce367d21f33e23a84fb83a641b3834dd7dd8e9318ad8ff677fbfae5915a239f7",
|
||||
"sha256:ce450ffbfec93821ab1fea94779a8440e10cf63819be6e176eb1973a6017aff5",
|
||||
"sha256:ce5cc53aa9fbbf6712e92c7cf268274eaff30f6bd12a0754e8133d85a8fb0f5f",
|
||||
"sha256:d466967ac8e45244b9dfe302bbe5e3337f8dc4dec8d7d10f5e950d83b140d33a",
|
||||
"sha256:d881c2e657c51d89f02ae4c21d9adbef76b8325fe4d5cf0e9ad62f850f3a98fd",
|
||||
"sha256:e565569fc28e3ba3e475ec344d87ed3cd8ba2d575335359749298a0899fe122e",
|
||||
"sha256:ea55b80eb0d1c3f1d8d784264a6764f931e172480a2f1868f2536444c5f01e01"
|
||||
"sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a",
|
||||
"sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938",
|
||||
"sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29",
|
||||
"sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae",
|
||||
"sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387",
|
||||
"sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a",
|
||||
"sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf",
|
||||
"sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610",
|
||||
"sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9",
|
||||
"sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5",
|
||||
"sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3",
|
||||
"sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89",
|
||||
"sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded",
|
||||
"sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754",
|
||||
"sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f",
|
||||
"sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868",
|
||||
"sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd",
|
||||
"sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910",
|
||||
"sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3",
|
||||
"sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac",
|
||||
"sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"
|
||||
],
|
||||
"version": "==2020.5.14"
|
||||
"version": "==2020.6.8"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
|
||||
],
|
||||
"version": "==2.24.0"
|
||||
},
|
||||
"selenium": {
|
||||
"hashes": [
|
||||
"sha256:5f5489a0c5fe2f09cc6bc3f32a0d53441ab36882c987269f2afe805979633ac1",
|
||||
"sha256:a9779ddc69cf03b75d94062c5e948f763919cf3341c77272f94cd05e6b4c7b32"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.0a6.post2"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
|
@ -1133,10 +1235,10 @@
|
|||
},
|
||||
"stevedore": {
|
||||
"hashes": [
|
||||
"sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b",
|
||||
"sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b"
|
||||
"sha256:609912b87df5ad338ff8e44d13eaad4f4170a65b79ae9cb0aa5632598994a1b7",
|
||||
"sha256:c4724f8d7b8f6be42130663855d01a9c2414d6046055b5a65ab58a0e38637688"
|
||||
],
|
||||
"version": "==1.32.0"
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
|
@ -1179,6 +1281,25 @@
|
|||
"index": "pypi",
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"urllib3": {
|
||||
"extras": [
|
||||
"secure"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
|
||||
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": null,
|
||||
"version": "==1.25.9"
|
||||
},
|
||||
"websocket-client": {
|
||||
"hashes": [
|
||||
"sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549",
|
||||
"sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010"
|
||||
],
|
||||
"version": "==0.57.0"
|
||||
},
|
||||
"wrapt": {
|
||||
"hashes": [
|
||||
"sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"
|
||||
|
|
21
README.md
|
@ -1,11 +1,12 @@
|
|||
# passbook
|
||||
<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">
|
||||
|
||||
![](https://img.shields.io/azure-devops/build/beryjuorg/5d94b893-6dea-4f68-a8fe-10f1674fc3a9/1?style=flat-square)
|
||||
![](https://img.shields.io/docker/pulls/beryju/passbook.svg?style=flat-square)
|
||||
![](https://img.shields.io/docker/pulls/beryju/passbook-gatekeeper.svg?style=flat-square)
|
||||
![](https://img.shields.io/docker/pulls/beryju/passbook-static.svg?style=flat-square)
|
||||
![](https://img.shields.io/docker/v/beryju/passbook?sort=semver&style=flat-square)
|
||||
![](https://img.shields.io/codecov/c/gh/beryju/passbook?style=flat-square)
|
||||
=======
|
||||
![CI Build status](https://img.shields.io/azure-devops/build/beryjuorg/5d94b893-6dea-4f68-a8fe-10f1674fc3a9/1?style=flat-square)
|
||||
![Docker pulls](https://img.shields.io/docker/pulls/beryju/passbook.svg?style=flat-square)
|
||||
![Docker pulls (gatekeeper)](https://img.shields.io/docker/pulls/beryju/passbook-gatekeeper.svg?style=flat-square)
|
||||
![Latest version](https://img.shields.io/docker/v/beryju/passbook?sort=semver&style=flat-square)
|
||||
![LGTM Grade](https://img.shields.io/lgtm/grade/python/github/BeryJu/passbook?style=flat-square)
|
||||
![Code Coverage](https://img.shields.io/codecov/c/gh/beryju/passbook?style=flat-square)
|
||||
|
||||
## What is passbook?
|
||||
|
||||
|
@ -28,12 +29,12 @@ docker-compose up -d
|
|||
docker-compose exec server ./manage.py migrate
|
||||
```
|
||||
|
||||
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://beryju.github.io/passbook/installation/kubernetes/)
|
||||
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://passbook.beryju.org//installation/kubernetes/)
|
||||
|
||||
## Screenshots
|
||||
|
||||
![](.github/screen_apps.png)
|
||||
![](.github/screen_admin.png)
|
||||
![](docs/images/screen_apps.png)
|
||||
![](docs/images/screen_admin.png)
|
||||
|
||||
## Development
|
||||
|
||||
|
|
|
@ -135,7 +135,23 @@ stages:
|
|||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run ./scripts/coverage.sh
|
||||
script: |
|
||||
cd e2e
|
||||
docker-compose pull -q chrome
|
||||
docker-compose up -d chrome
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
cd passbook/static/static
|
||||
yarn
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run coverage run ./manage.py test --failfast
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
PathtoPublish: 'selenium_screenshots/'
|
||||
ArtifactName: 'drop'
|
||||
publishLocation: 'Container'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run coverage xml
|
||||
|
@ -156,7 +172,7 @@ stages:
|
|||
repository: 'beryju/passbook'
|
||||
command: 'buildAndPush'
|
||||
Dockerfile: 'Dockerfile'
|
||||
tags: '$(Build.SourceBranchName)'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
- job: build_gatekeeper
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
@ -170,7 +186,7 @@ stages:
|
|||
repository: 'beryju/passbook-gatekeeper'
|
||||
command: 'buildAndPush'
|
||||
Dockerfile: 'Dockerfile'
|
||||
tags: '$(Build.SourceBranchName)'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
- job: build_static
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
@ -187,11 +203,11 @@ stages:
|
|||
repository: 'beryju/passbook-static'
|
||||
command: 'build'
|
||||
Dockerfile: 'static.Dockerfile'
|
||||
tags: '$(Build.SourceBranchName)'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
arguments: "--network=beryjupassbook_default"
|
||||
- task: Docker@2
|
||||
inputs:
|
||||
containerRegistry: 'dockerhub'
|
||||
repository: 'beryju/passbook-static'
|
||||
command: 'push'
|
||||
tags: '$(Build.SourceBranchName)'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
|
|
|
@ -67,13 +67,13 @@ services:
|
|||
- traefik.docker.network=internal
|
||||
traefik:
|
||||
image: traefik:1.7
|
||||
command: --api --docker
|
||||
command: --api --docker --defaultentrypoints=https --entryPoints='Name:http Address::80 Redirect.EntryPoint:https' --entryPoints='Name:https Address::443 TLS'
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
ports:
|
||||
- "0.0.0.0:80:80"
|
||||
- "0.0.0.0:443:443"
|
||||
- "0.0.0.0:8080:8080"
|
||||
- "127.0.0.1:8080:8080"
|
||||
networks:
|
||||
- internal
|
||||
|
||||
|
|
9
docker.env.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
debug: true
|
||||
postgresql:
|
||||
user: postgres
|
||||
host: postgresql
|
||||
|
||||
redis:
|
||||
host: redis
|
||||
|
||||
log_level: debug
|
|
@ -1,14 +0,0 @@
|
|||
FROM python:3.8-slim-buster as builder
|
||||
|
||||
WORKDIR /mkdocs
|
||||
|
||||
RUN pip install mkdocs mkdocs-material
|
||||
|
||||
COPY docs/ docs
|
||||
COPY mkdocs.yml .
|
||||
|
||||
RUN mkdocs build
|
||||
|
||||
FROM nginx
|
||||
|
||||
COPY --from=builder /mkdocs/site /usr/share/nginx/html
|
3
docs/build.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash -x
|
||||
pip install -U mkdocs mkdocs-material
|
||||
mkdocs gh-deploy
|
55
docs/expressions/index.md
Normal file
|
@ -0,0 +1,55 @@
|
|||
# Expressions
|
||||
|
||||
Expressions allow you to write custom logic using Python code.
|
||||
|
||||
Expressions are used in different places throughout passbook, and can do different things.
|
||||
|
||||
!!! info
|
||||
These functions/objects are available wherever expressions are used. For more specific information, see [Expression Policies](../policies/expression.md) and [Property Mappings](../property-mappings/expression.md)
|
||||
|
||||
## Global objects
|
||||
|
||||
- `pb_logger`: structlog BoundLogger. ([ref](https://www.structlog.org/en/stable/api.html#structlog.BoundLogger))
|
||||
- `requests`: requests Session object. ([ref](https://requests.readthedocs.io/en/master/user/advanced/))
|
||||
|
||||
## Generally available functions
|
||||
|
||||
### `regex_match(value: Any, regex: str) -> bool`
|
||||
|
||||
Check if `value` matches Regular Expression `regex`.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
return regex_match(request.user.username, '.*admin.*')
|
||||
```
|
||||
|
||||
### `regex_replace(value: Any, regex: str, repl: str) -> str`
|
||||
|
||||
Replace anything matching `regex` within `value` with `repl` and return it.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
user_email_local = regex_replace(request.user.email, '(.+)@.+', '')
|
||||
```
|
||||
|
||||
### `pb_is_group_member(user: User, **group_filters) -> bool`
|
||||
|
||||
Check if `user` is member of a group matching `**group_filters`.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
return pb_is_group_member(request.user, name="test_group")
|
||||
```
|
||||
|
||||
### `pb_user_by(**filters) -> Optional[User]`
|
||||
|
||||
Fetch a user matching `**filters`. Returns "None" if no user was found.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
other_user = pb_user_by(username="other_user")
|
||||
```
|
21
docs/expressions/reference/user-object.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# 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,23 +0,0 @@
|
|||
# Factors
|
||||
|
||||
A factor represents a single authenticating factor for a user. Common examples of this would be a password or an OTP. These factors can be combined in any order, and can be dynamically enabled using policies.
|
||||
|
||||
## Password Factor
|
||||
|
||||
This is the standard Password Factor. It allows you to select which Backend the password is checked with. here you can also specify which Policies are used to check the password. You can also specify which Factors a User has to pass to recover their account.
|
||||
|
||||
## Dummy Factor
|
||||
|
||||
This factor waits a random amount of time. Mostly used for debugging.
|
||||
|
||||
## E-Mail Factor
|
||||
|
||||
This factor is mostly for recovery, and used in conjunction with the Password Factor.
|
||||
|
||||
## OTP Factor
|
||||
|
||||
This is your typical One-Time Password implementation, compatible with Authy and Google Authenticator. You can enfore this Factor so that every user has to configure it, or leave it optional.
|
||||
|
||||
## Captcha Factor
|
||||
|
||||
While this factor doesn't really authenticate a user, it is part of the Authentication Flow. passbook uses Google's reCaptcha implementation.
|
36
docs/flow/examples/login.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Login Flow
|
||||
|
||||
This document describes how a simple authentication flow can be created.
|
||||
|
||||
This flow is created automatically when passbook is installed.
|
||||
|
||||
1. Create an **Identification** stage
|
||||
|
||||
> Here you can select whichever fields the user can identify themselves with
|
||||
> Select the Template **Default Login**, as this template shows the (optional) Flows
|
||||
> Here you can also link optional enrollment and recovery flows.
|
||||
|
||||
2. Create a **Password** stage
|
||||
|
||||
> Select the Backend you want the password to be checked against. Select "passbook-internal Userdatabase".
|
||||
|
||||
3. Create a **User Login** stage
|
||||
|
||||
> This stage doesn't have any options.
|
||||
|
||||
4. Create a flow
|
||||
|
||||
> Create a flow with the delegation of **Authentication**
|
||||
> Assign a name and a slug. The slug is used in the URL when the flow is executed.
|
||||
|
||||
5. Bind the stages to the flow
|
||||
|
||||
> Bind the **Identification** Stage with an order of 0
|
||||
> Bind the **Password** Stage with an order of 1
|
||||
> Bind the **User Login** Stage with an order of 2
|
||||
|
||||
![](login.png)
|
||||
|
||||
!!! notice
|
||||
|
||||
This flow can used by any user, authenticated and un-authenticated. This means any authenticated user that visits this flow can login again.
|
BIN
docs/flow/examples/login.png
Normal file
After Width: | Height: | Size: 110 KiB |
45
docs/flow/flows.md
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Flows
|
||||
|
||||
Flows are a method of describing a sequence of stages. A stage represents a single verification or logic step. They are used to authenticate users, enroll them, and more.
|
||||
|
||||
Upon flow execution, a plan containing all stages is generated. This means that all attached policies are evaluated upon execution. This behaviour can be altered by enabling the **Re-evaluate Policies** option on the binding.
|
||||
|
||||
To determine which flow is linked, passbook searches all flows with the required designation and chooses the first instance the current user has access to.
|
||||
|
||||
## Permissions
|
||||
|
||||
Flows can have policies assigned to them. These policies determine if the current user is allowed to see and use this flow.
|
||||
|
||||
## Designation
|
||||
|
||||
Flows are designated for a single purpose. This designation changes when a flow is used. The following designations are available:
|
||||
|
||||
### Authentication
|
||||
|
||||
This is designates a flow to be used for authentication.
|
||||
|
||||
The authentication flow should always contain a [**User Login**](stages/user_login.md) stage, which attaches the staged user to the current session.
|
||||
|
||||
### Invalidation
|
||||
|
||||
This designates a flow to be used to invalidate a session.
|
||||
|
||||
This stage should always contain a [**User Logout**](stages/user_logout.md) stage, which resets the current session.
|
||||
|
||||
### Enrollment
|
||||
|
||||
This designates a flow for enrollment. This flow can contain any amount of verification stages, such as [**email**](stages/email/index.md) or [**captcha**](stages/captcha/index.md). At the end, to create the user, you can use the [**user_write**](stages/user_write.md) stage, which either updates the currently staged user, or if none exists, creates a new one.
|
||||
|
||||
### Unenrollment
|
||||
|
||||
This designates a flow for unenrollment. This flow can contain any amount of verification stages, such as [**email**](stages/email/index.md) or [**captcha**](stages/captcha/index.md). As a final stage, to delete the account, use the [**user_delete**](stages/user_delete.md) stage.
|
||||
|
||||
### Recovery
|
||||
|
||||
This designates a flow for recovery. This flow normally contains an [**identification**](stages/identification/index.md) stage to find the user. It can also contain any amount of verification stages, such as [**email**](stages/email/index.md) or [**captcha**](stages/captcha/index.md).
|
||||
Afterwards, use the [**prompt**](stages/prompt/index.md) stage to ask the user for a new password and the [**user_write**](stages/user_write.md) stage to update the password.
|
||||
|
||||
### Change Password
|
||||
|
||||
This designates a flow for password changes. This flow can contain any amount of verification stages, such as [**email**](stages/email/index.md) or [**captcha**](stages/captcha/index.md).
|
||||
Afterwards, use the [**prompt**](stages/prompt/index.md) stage to ask the user for a new password and the [**user_write**](stages/user_write.md) stage to update the password.
|
BIN
docs/flow/stages/captcha/captcha-admin.png
Normal file
After Width: | Height: | Size: 140 KiB |
7
docs/flow/stages/captcha/index.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Captcha stage
|
||||
|
||||
This stage adds a form of verification using [Google's ReCaptcha](https://www.google.com/recaptcha/intro/v3.html).
|
||||
|
||||
This stage has two required fields: Public key and private key. These can both be acquired at https://www.google.com/recaptcha/admin.
|
||||
|
||||
![](captcha-admin.png)
|
BIN
docs/flow/stages/dummy/dummy.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
5
docs/flow/stages/dummy/index.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Dummy stage
|
||||
|
||||
This stage is used for development and has no function. It presents the user with a form which requires a single confirmation.
|
||||
|
||||
![](dummy.png)
|
BIN
docs/flow/stages/email/email-recovery.png
Normal file
After Width: | Height: | Size: 51 KiB |
5
docs/flow/stages/email/index.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Email
|
||||
|
||||
This stage can be used for email verification. passbook's background worker will send an email using the specified connection details. When an email can't be delivered, delivery is automatically retried periodically.
|
||||
|
||||
![](email-recovery.png)
|
25
docs/flow/stages/identification/index.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Identification
|
||||
|
||||
This stage provides a ready-to-go form for users to identify themselves.
|
||||
|
||||
## Options
|
||||
|
||||
### User Fields
|
||||
|
||||
Select which fields the user can use to identify themselves. Multiple fields can be specified and separated with a comma.
|
||||
Valid choices:
|
||||
|
||||
- email
|
||||
- username
|
||||
|
||||
### Template
|
||||
|
||||
This specifies which template is rendered. Currently there are two templates:
|
||||
|
||||
The `Login` template shows configured Sources below the login form, as well as linking to the defined Enrollment and Recovery flows.
|
||||
|
||||
The `Recovery` template shows only the form.
|
||||
|
||||
### Enrollment/Recovery Flow
|
||||
|
||||
These fields specify if and which flows are linked on the form. The enrollment flow is linked as `Need an account? Sign up.`, and the recovery flow is linked as `Forgot username or password?`.
|
7
docs/flow/stages/invitation/index.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Invitation Stage
|
||||
|
||||
This stage can be used to invite users. You can use this to enroll users with preset values.
|
||||
|
||||
If the option `Continue Flow without Invitation` is enabled, this stage will continue even when no invitation token is present.
|
||||
|
||||
To check if a user has used an invitation within a policy, you can check `request.context.invitation_in_effect`.
|
7
docs/flow/stages/otp/index.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# 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.
|
3
docs/flow/stages/password/index.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Password Stage
|
||||
|
||||
This is a generic password prompt which authenticates the current `pending_user`. This stage allows the selection of the source the user is authenticated against.
|
42
docs/flow/stages/prompt/index.md
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Prompt Stage
|
||||
|
||||
This stage is used to show the user arbitrary prompts.
|
||||
|
||||
## Prompt
|
||||
|
||||
The prompt can be any of the following types:
|
||||
|
||||
| Type | Description |
|
||||
|----------|------------------------------------------------------------------|
|
||||
| text | Arbitrary text. No client-side validation is done. |
|
||||
| email | Email input. Requires a valid email adress. |
|
||||
| password | Password input. |
|
||||
| number | Number input. Any number is allowed. |
|
||||
| checkbox | Simple checkbox. |
|
||||
| hidden | Hidden input field. Allows for the pre-setting of default values.|
|
||||
|
||||
A prompt has the following attributes:
|
||||
|
||||
### `field_key`
|
||||
|
||||
The HTML name used for the prompt. This key is also used to later retrieve the data in expression policies:
|
||||
|
||||
```python
|
||||
request.context.get('prompt_data').get('<field_key>')
|
||||
```
|
||||
|
||||
### `label`
|
||||
|
||||
The label used to describe the field. Depending on the selected template, this may not be shown.
|
||||
|
||||
### `required`
|
||||
|
||||
A flag which decides whether or not this field is required.
|
||||
|
||||
### `placeholder`
|
||||
|
||||
A field placeholder, shown within the input field. This field is also used by the `hidden` type as the actual value.
|
||||
|
||||
### `order`
|
||||
|
||||
The numerical index of the prompt. This applies to all stages which this prompt is a part of.
|
16
docs/flow/stages/prompt/validation.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Prompt Validation
|
||||
|
||||
Further validation of prompts can be done using policies.
|
||||
|
||||
To validate that two password fields are identical, create the following expression policy:
|
||||
|
||||
```python
|
||||
if request.context.get('prompt_data').get('password') == request.context.get('prompt_data').get('password_repeat'):
|
||||
return True
|
||||
|
||||
pb_message("Passwords don't match.")
|
||||
return False
|
||||
```
|
||||
This policy expects you to have two password fields with `field_key` set to `password` and `password_repeat`.
|
||||
|
||||
Afterwards, bind this policy to the prompt stage you want to validate.
|
8
docs/flow/stages/user_delete.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# 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.
|
5
docs/flow/stages/user_login.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# User Login Stage
|
||||
|
||||
This stage attaches a currently pending user to the current session.
|
||||
|
||||
It can be used after `user_write` during an enrollment flow, or after a `password` stage during an authentication flow.
|
3
docs/flow/stages/user_logout.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# User Logout Stage
|
||||
|
||||
Opposite stage of [User Login Stages](user_login.md). It removes the user from the current session.
|
3
docs/flow/stages/user_write.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# User Write Stage
|
||||
|
||||
This stages writes data from the current context to the current pending user. If no user is pending, a new one is created.
|
2
docs/images/brand.svg
Normal file
|
@ -0,0 +1,2 @@
|
|||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="120px" height="20px" viewBox="15 0 10 10" enable-background="new 0 0 270 10" xml:space="preserve"><defs><style>.cls-1{isolation:isolate;}.cls-2{fill:#fff;}</style></defs><g class="cls-1"><path class="cls-2" d="M1.65,11V2.45H2.87V3a2.81,2.81,0,0,1,.47-.45A1.13,1.13,0,0,1,4,2.38,1.11,1.11,0,0,1,5.1,3a1.55,1.55,0,0,1,.16.5,5.61,5.61,0,0,1,0,.81V6.58c0,.45,0,.77,0,1a1.17,1.17,0,0,1-.55.9,1.23,1.23,0,0,1-.7.16,1.35,1.35,0,0,1-.64-.16A1.53,1.53,0,0,1,2.89,8h0v3ZM4.08,4.43a1.21,1.21,0,0,0-.14-.6.51.51,0,0,0-.46-.22A.54.54,0,0,0,3,3.82a.8.8,0,0,0-.17.54V6.73A.68.68,0,0,0,3,7.2a.6.6,0,0,0,.44.18A.53.53,0,0,0,4,7.17a1,1,0,0,0,.12-.5Z"/><path class="cls-2" d="M8.63,8.54V7.91h0a2.24,2.24,0,0,1-.48.52,1.13,1.13,0,0,1-.69.18A1.39,1.39,0,0,1,7,8.54a1.09,1.09,0,0,1-.43-.24,1.32,1.32,0,0,1-.33-.49A2.33,2.33,0,0,1,6.11,7a4.89,4.89,0,0,1,.08-.91,1.51,1.51,0,0,1,.31-.65,1.44,1.44,0,0,1,.59-.38A3.19,3.19,0,0,1,8,4.93h.59V4.33a1,1,0,0,0-.13-.52A.52.52,0,0,0,8,3.61a.71.71,0,0,0-.44.15.78.78,0,0,0-.26.46H6.13A2,2,0,0,1,6.69,2.9a1.73,1.73,0,0,1,.57-.38A2,2,0,0,1,8,2.38a2.18,2.18,0,0,1,.72.12,1.71,1.71,0,0,1,.59.36,2,2,0,0,1,.38.6,2.18,2.18,0,0,1,.14.84V8.54Zm0-2.62-.34,0a1.2,1.2,0,0,0-.67.18.76.76,0,0,0-.29.68.89.89,0,0,0,.17.56A.55.55,0,0,0,8,7.53a.63.63,0,0,0,.49-.2.91.91,0,0,0,.17-.58Z"/><path class="cls-2" d="M13,4.16a.59.59,0,0,0-.2-.47.65.65,0,0,0-.42-.16.59.59,0,0,0-.45.19.66.66,0,0,0-.15.43.8.8,0,0,0,.08.33.85.85,0,0,0,.44.29l.71.29a1.73,1.73,0,0,1,.95.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.56,1.56,0,0,1-.58.39,1.88,1.88,0,0,1-2-.32,1.58,1.58,0,0,1-.4-.57,1.81,1.81,0,0,1-.17-.8h1.15a1.11,1.11,0,0,0,.17.47.56.56,0,0,0,.49.22.71.71,0,0,0,.47-.18A.59.59,0,0,0,13,6.8a.69.69,0,0,0-.13-.43,1.08,1.08,0,0,0-.48-.32l-.59-.21a2.08,2.08,0,0,1-.9-.64,1.66,1.66,0,0,1-.33-1,1.89,1.89,0,0,1,.14-.72,1.78,1.78,0,0,1,.4-.57,1.5,1.5,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.6,1.6,0,0,1,.54.38,1.85,1.85,0,0,1,.36.57,1.82,1.82,0,0,1,.13.7Z"/><path class="cls-2" d="M17.2,4.16a.63.63,0,0,0-.2-.47.69.69,0,0,0-.43-.16.55.55,0,0,0-.44.19.62.62,0,0,0-.16.43.68.68,0,0,0,.09.33.81.81,0,0,0,.43.29l.72.29a1.7,1.7,0,0,1,.94.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.61,1.61,0,0,1-.57.39,1.81,1.81,0,0,1-.74.15,1.76,1.76,0,0,1-1.24-.47,1.61,1.61,0,0,1-.41-.57,2,2,0,0,1-.17-.8h1.15a1.12,1.12,0,0,0,.18.47.53.53,0,0,0,.48.22.72.72,0,0,0,.48-.18.59.59,0,0,0,.21-.48.69.69,0,0,0-.14-.43,1,1,0,0,0-.48-.32l-.58-.21a2.06,2.06,0,0,1-.91-.64,1.66,1.66,0,0,1-.33-1A1.89,1.89,0,0,1,15,3.44a1.78,1.78,0,0,1,.4-.57,1.58,1.58,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.75,1.75,0,0,1,.55.38,1.85,1.85,0,0,1,.36.57,2,2,0,0,1,.13.7Z"/><path class="cls-2" d="M19.2,8.54V0h1.22V3h0a1.53,1.53,0,0,1,.48-.47,1.39,1.39,0,0,1,.65-.16,1.26,1.26,0,0,1,.69.16,1.35,1.35,0,0,1,.4.39,1.18,1.18,0,0,1,.15.51,7.72,7.72,0,0,1,0,1V6.73a5.56,5.56,0,0,1-.05.8,1.56,1.56,0,0,1-.15.5,1.12,1.12,0,0,1-1.07.58,1.15,1.15,0,0,1-.7-.18A3.79,3.79,0,0,1,20.42,8v.55Zm2.44-4.21a1,1,0,0,0-.13-.51A.5.5,0,0,0,21,3.61a.57.57,0,0,0-.44.18.66.66,0,0,0-.18.48V6.63a.83.83,0,0,0,.17.54.52.52,0,0,0,.45.21.49.49,0,0,0,.45-.22,1.11,1.11,0,0,0,.15-.6Z"/><path class="cls-2" d="M23.76,4.49a4.83,4.83,0,0,1,0-.68A1.55,1.55,0,0,1,24,3.26a1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24A1.59,1.59,0,0,1,24,7.73a1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1,0-.68ZM25,6.69a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17A.55.55,0,0,0,26,7.21a.72.72,0,0,0,.16-.52V4.3A.74.74,0,0,0,26,3.78a.55.55,0,0,0-.44-.17.53.53,0,0,0-.43.17A.74.74,0,0,0,25,4.3Z"/><path class="cls-2" d="M28.2,4.49a4.83,4.83,0,0,1,.05-.68,1.55,1.55,0,0,1,.18-.55,1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24,1.59,1.59,0,0,1-.62-.64,1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1-.05-.68Zm1.22,2.2a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17.55.55,0,0,0,.44-.17.72.72,0,0,0,.16-.52V4.3a.74.74,0,0,0-.16-.52A.55.55,0,0,0,30,3.61a.53.53,0,0,0-.43.17.74.74,0,0,0-.17.52Z"/><path class="cls-2" d="M32.75,8.54V0H34V5.11h0l1.47-2.66H36.7L35.24,4.93,37,8.54H35.66l-1.1-2.63L34,6.83V8.54Z"/></g></svg>
|
After Width: | Height: | Size: 4.5 KiB |
2
docs/images/brand_inverted.svg
Normal file
|
@ -0,0 +1,2 @@
|
|||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="120px" height="20px" viewBox="15 0 10 10" enable-background="new 0 0 270 10" xml:space="preserve"><defs><style>.cls-1{isolation:isolate;}.cls-2{fill:#000;}</style></defs><g class="cls-1"><path class="cls-2" d="M1.65,11V2.45H2.87V3a2.81,2.81,0,0,1,.47-.45A1.13,1.13,0,0,1,4,2.38,1.11,1.11,0,0,1,5.1,3a1.55,1.55,0,0,1,.16.5,5.61,5.61,0,0,1,0,.81V6.58c0,.45,0,.77,0,1a1.17,1.17,0,0,1-.55.9,1.23,1.23,0,0,1-.7.16,1.35,1.35,0,0,1-.64-.16A1.53,1.53,0,0,1,2.89,8h0v3ZM4.08,4.43a1.21,1.21,0,0,0-.14-.6.51.51,0,0,0-.46-.22A.54.54,0,0,0,3,3.82a.8.8,0,0,0-.17.54V6.73A.68.68,0,0,0,3,7.2a.6.6,0,0,0,.44.18A.53.53,0,0,0,4,7.17a1,1,0,0,0,.12-.5Z"/><path class="cls-2" d="M8.63,8.54V7.91h0a2.24,2.24,0,0,1-.48.52,1.13,1.13,0,0,1-.69.18A1.39,1.39,0,0,1,7,8.54a1.09,1.09,0,0,1-.43-.24,1.32,1.32,0,0,1-.33-.49A2.33,2.33,0,0,1,6.11,7a4.89,4.89,0,0,1,.08-.91,1.51,1.51,0,0,1,.31-.65,1.44,1.44,0,0,1,.59-.38A3.19,3.19,0,0,1,8,4.93h.59V4.33a1,1,0,0,0-.13-.52A.52.52,0,0,0,8,3.61a.71.71,0,0,0-.44.15.78.78,0,0,0-.26.46H6.13A2,2,0,0,1,6.69,2.9a1.73,1.73,0,0,1,.57-.38A2,2,0,0,1,8,2.38a2.18,2.18,0,0,1,.72.12,1.71,1.71,0,0,1,.59.36,2,2,0,0,1,.38.6,2.18,2.18,0,0,1,.14.84V8.54Zm0-2.62-.34,0a1.2,1.2,0,0,0-.67.18.76.76,0,0,0-.29.68.89.89,0,0,0,.17.56A.55.55,0,0,0,8,7.53a.63.63,0,0,0,.49-.2.91.91,0,0,0,.17-.58Z"/><path class="cls-2" d="M13,4.16a.59.59,0,0,0-.2-.47.65.65,0,0,0-.42-.16.59.59,0,0,0-.45.19.66.66,0,0,0-.15.43.8.8,0,0,0,.08.33.85.85,0,0,0,.44.29l.71.29a1.73,1.73,0,0,1,.95.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.56,1.56,0,0,1-.58.39,1.88,1.88,0,0,1-2-.32,1.58,1.58,0,0,1-.4-.57,1.81,1.81,0,0,1-.17-.8h1.15a1.11,1.11,0,0,0,.17.47.56.56,0,0,0,.49.22.71.71,0,0,0,.47-.18A.59.59,0,0,0,13,6.8a.69.69,0,0,0-.13-.43,1.08,1.08,0,0,0-.48-.32l-.59-.21a2.08,2.08,0,0,1-.9-.64,1.66,1.66,0,0,1-.33-1,1.89,1.89,0,0,1,.14-.72,1.78,1.78,0,0,1,.4-.57,1.5,1.5,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.6,1.6,0,0,1,.54.38,1.85,1.85,0,0,1,.36.57,1.82,1.82,0,0,1,.13.7Z"/><path class="cls-2" d="M17.2,4.16a.63.63,0,0,0-.2-.47.69.69,0,0,0-.43-.16.55.55,0,0,0-.44.19.62.62,0,0,0-.16.43.68.68,0,0,0,.09.33.81.81,0,0,0,.43.29l.72.29a1.7,1.7,0,0,1,.94.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.61,1.61,0,0,1-.57.39,1.81,1.81,0,0,1-.74.15,1.76,1.76,0,0,1-1.24-.47,1.61,1.61,0,0,1-.41-.57,2,2,0,0,1-.17-.8h1.15a1.12,1.12,0,0,0,.18.47.53.53,0,0,0,.48.22.72.72,0,0,0,.48-.18.59.59,0,0,0,.21-.48.69.69,0,0,0-.14-.43,1,1,0,0,0-.48-.32l-.58-.21a2.06,2.06,0,0,1-.91-.64,1.66,1.66,0,0,1-.33-1A1.89,1.89,0,0,1,15,3.44a1.78,1.78,0,0,1,.4-.57,1.58,1.58,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.75,1.75,0,0,1,.55.38,1.85,1.85,0,0,1,.36.57,2,2,0,0,1,.13.7Z"/><path class="cls-2" d="M19.2,8.54V0h1.22V3h0a1.53,1.53,0,0,1,.48-.47,1.39,1.39,0,0,1,.65-.16,1.26,1.26,0,0,1,.69.16,1.35,1.35,0,0,1,.4.39,1.18,1.18,0,0,1,.15.51,7.72,7.72,0,0,1,0,1V6.73a5.56,5.56,0,0,1-.05.8,1.56,1.56,0,0,1-.15.5,1.12,1.12,0,0,1-1.07.58,1.15,1.15,0,0,1-.7-.18A3.79,3.79,0,0,1,20.42,8v.55Zm2.44-4.21a1,1,0,0,0-.13-.51A.5.5,0,0,0,21,3.61a.57.57,0,0,0-.44.18.66.66,0,0,0-.18.48V6.63a.83.83,0,0,0,.17.54.52.52,0,0,0,.45.21.49.49,0,0,0,.45-.22,1.11,1.11,0,0,0,.15-.6Z"/><path class="cls-2" d="M23.76,4.49a4.83,4.83,0,0,1,0-.68A1.55,1.55,0,0,1,24,3.26a1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24A1.59,1.59,0,0,1,24,7.73a1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1,0-.68ZM25,6.69a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17A.55.55,0,0,0,26,7.21a.72.72,0,0,0,.16-.52V4.3A.74.74,0,0,0,26,3.78a.55.55,0,0,0-.44-.17.53.53,0,0,0-.43.17A.74.74,0,0,0,25,4.3Z"/><path class="cls-2" d="M28.2,4.49a4.83,4.83,0,0,1,.05-.68,1.55,1.55,0,0,1,.18-.55,1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24,1.59,1.59,0,0,1-.62-.64,1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1-.05-.68Zm1.22,2.2a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17.55.55,0,0,0,.44-.17.72.72,0,0,0,.16-.52V4.3a.74.74,0,0,0-.16-.52A.55.55,0,0,0,30,3.61a.53.53,0,0,0-.43.17.74.74,0,0,0-.17.52Z"/><path class="cls-2" d="M32.75,8.54V0H34V5.11h0l1.47-2.66H36.7L35.24,4.93,37,8.54H35.66l-1.1-2.63L34,6.83V8.54Z"/></g></svg>
|
After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
|
@ -1,31 +1,16 @@
|
|||
# Welcome
|
||||
#
|
||||
![passbook logo](images/logo.svg){: style="height:50px"}
|
||||
![passbook brand](images/brand.svg){: style="height:50px"}
|
||||
|
||||
Welcome to the passbook Documentation. passbook is an open-source Identity Provider and Usermanagement software. It can be used as a central directory for users or customers and it can integrate with your existing Directory.
|
||||
## What is passbook?
|
||||
|
||||
passbook can also be used as part of an Application to facilitate User Enrollment, Password recovery and Social Login.
|
||||
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.
|
||||
|
||||
passbook uses the following Terminology:
|
||||
## Installation
|
||||
|
||||
### Policy
|
||||
See [Docker-compose](installation/docker-compose.md) or [Kubernetes](installation/kubernetes.md)
|
||||
|
||||
A Policy is at a base level a yes/no gate. It will either evaluate to True or False depending on the Policy Kind and settings. For example, a "Group Membership Policy" evaluates to True if the User is member of the specified Group and False if not. This can be used to conditionally apply Factors and grant/deny access.
|
||||
## Screenshots
|
||||
|
||||
### Provider
|
||||
|
||||
A Provider is a way for other Applications to authenticate against passbook. Common Providers are OpenID Connect (OIDC) and SAML.
|
||||
|
||||
### Source
|
||||
|
||||
Sources are ways to get users into passbook. This might be an LDAP Connection to import Users from Active Directory, or an OAuth2 Connection to allow Social Logins.
|
||||
|
||||
### Application
|
||||
|
||||
An application links together Policies with a Provider, allowing you to control access. It also holds Information like UI Name, Icon and more.
|
||||
|
||||
### Factors
|
||||
|
||||
Factors represent Authentication Factors, like a Password or OTP. These Factors can be dynamically enabled using policies. This allows you to, for example, force users from a certain IP ranges to complete a Captcha to authenticate.
|
||||
|
||||
### Property Mappings
|
||||
|
||||
Property Mappings allow you to make Information available for external Applications. For example, if you want to login to AWS with passbook, you'd use Property Mappings to set the User's Roles based on their Groups.
|
||||
![](images/screen_apps.png)
|
||||
![](images/screen_admin.png)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# docker-compose
|
||||
|
||||
This installation Method is for test-setups and small-scale productive setups.
|
||||
This installation method is for test-setups and small-scale productive setups.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
@ -11,16 +11,25 @@ This installation Method is for test-setups and small-scale productive setups.
|
|||
|
||||
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml). Place it in a directory of your choice.
|
||||
|
||||
passbook needs to know it's primary URL to create links in E-Mails and set cookies, so you have to run the following command:
|
||||
|
||||
```
|
||||
export PASSBOOK_DOMAIN=domain.tld # this can be any domain or IP, it just needs to point to passbook.
|
||||
wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
|
||||
# Optionally enable Error-reporting
|
||||
# export PASSBOOK_ERROR_REPORTING=true
|
||||
# Optionally deploy a different version
|
||||
# export PASSBOOK_TAG=0.8.15-beta
|
||||
# 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 exec server ./manage.py migrate
|
||||
```
|
||||
|
||||
The compose file references the current latest version, which can be overridden with the `SERVER_TAG` Environment variable.
|
||||
The compose file references the current latest version, which can be overridden with the `SERVER_TAG` environment variable.
|
||||
|
||||
If you plan to use this setup for production, it is also advised to change the PostgreSQL Password by setting `PG_PASS` to a password of your choice.
|
||||
If you plan to use this setup for production, it is also advised to change the PostgreSQL password by setting `PG_PASS` to a password of your choice.
|
||||
|
||||
Now you can pull the Docker images needed by running `docker-compose pull`. After this has finished, run `docker-compose up -d` to start passbook.
|
||||
|
||||
passbook will then be reachable on Port 80. You can optionally configure the packaged traefik to use Let's Encrypt for TLS Encryption.
|
||||
passbook will then be reachable via HTTP on port 80, and HTTPS on port 443. You can optionally configure the packaged traefik to use Let's Encrypt certificates for TLS Encryption.
|
||||
|
||||
The initial setup process also creates a default admin user, the username and password for which is `pbadmin`. It is highly recommended to change this password as soon as you log in.
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
# Installation
|
||||
|
||||
There are two supported ways to install passbook:
|
||||
|
||||
- [docker-compose](docker-compose.md) for test- or small productive setups
|
||||
- [Kubernetes](./kubernetes.md) for larger Productive setups
|
|
@ -1,6 +1,8 @@
|
|||
# Kubernetes
|
||||
|
||||
For a mid to high-load Installation, Kubernetes is recommended. passbook is installed using a helm-chart.
|
||||
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.
|
||||
|
||||
```
|
||||
# Default values for passbook.
|
||||
|
|
|
@ -9,19 +9,19 @@
|
|||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `passbook.company` is the FQDN of the passbook Install
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Create an application in passbook and note the slug, as this will be used later. Create a SAML Provider with the following Parameters:
|
||||
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`
|
||||
|
||||
You can of course use a custom Signing Certificate, and adjust durations.
|
||||
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.
|
||||
Create a role with the permissions you desire, and note the ARN.
|
||||
|
||||
AWS requires two custom PropertyMappings; `Role` and `RoleSessionName`. Create them as following:
|
||||
|
||||
|
@ -29,4 +29,4 @@ AWS requires two custom PropertyMappings; `Role` and `RoleSessionName`. Create t
|
|||
|
||||
![](./property-mapping-role-session-name.png)
|
||||
|
||||
Afterwards export the Metadata from passbook, and create an Identity Provider [here](https://console.aws.amazon.com/iam/home#/providers).
|
||||
Afterwards export the metadata from passbook, and create an Identity Provider [here](https://console.aws.amazon.com/iam/home#/providers).
|
||||
|
|
|
@ -14,13 +14,13 @@ The following placeholders will be used:
|
|||
- `gitlab.company` is the FQDN of the GitLab Install
|
||||
- `passbook.company` is the FQDN of the passbook Install
|
||||
|
||||
Create an application in passbook and note the slug, as this will be used later. Create a SAML Provider with the following Parameters:
|
||||
Create an application in passbook and note the slug, as this will be used later. Create a SAML provider with the following parameters:
|
||||
|
||||
- ACS URL: `https://gitlab.company/users/auth/saml/callback`
|
||||
- Audience: `https://gitlab.company`
|
||||
- Issuer: `https://gitlab.company`
|
||||
|
||||
You can of course use a custom Signing Certificate, and adjust durations. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php).
|
||||
You can of course use a custom signing certificate, and adjust durations. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php).
|
||||
|
||||
## GitLab Configuration
|
||||
|
||||
|
|
|
@ -11,10 +11,10 @@ From https://goharbor.io
|
|||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `harbor.company` is the FQDN of the Harbor Install
|
||||
- `passbook.company` is the FQDN of the passbook Install
|
||||
- `harbor.company` is the FQDN of the Harbor install.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Create an application in passbook. Create an OpenID Provider with the following Parameters:
|
||||
Create an application in passbook. Create an OpenID provider with the following parameters:
|
||||
|
||||
- Client Type: `Confidential`
|
||||
- Response types: `code (Authorization Code Flow)`
|
||||
|
|
|
@ -5,23 +5,23 @@
|
|||
From https://rancher.com/products/rancher
|
||||
|
||||
!!! note ""
|
||||
An Enterprise Platform for Managing Kubernetes Everywhere
|
||||
An enterprise platform for managing Kubernetes Everywhere
|
||||
Rancher is a platform built to address the needs of the DevOps teams deploying applications with Kubernetes, and the IT staff responsible for delivering an enterprise-critical service.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `rancher.company` is the FQDN of the Rancher Install
|
||||
- `passbook.company` is the FQDN of the passbook Install
|
||||
- `rancher.company` is the FQDN of the Rancher 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:
|
||||
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://rancher.company/v1-saml/adfs/saml/acs`
|
||||
- Audience: `https://rancher.company/v1-saml/adfs/saml/metadata`
|
||||
- Issuer: `passbook`
|
||||
|
||||
You can of course use a custom Signing Certificate, and adjust durations.
|
||||
You can of course use a custom signing certificate, and adjust durations.
|
||||
|
||||
## Rancher
|
||||
|
||||
|
|
|
@ -15,10 +15,10 @@ From https://sentry.io
|
|||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `sentry.company` is the FQDN of the Sentry Install
|
||||
- `passbook.company` is the FQDN of the passbook Install
|
||||
- `sentry.company` is the FQDN of the Sentry install.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Create an application in passbook. Create an OpenID Provider with the following Parameters:
|
||||
Create an application in passbook. Create an OpenID provider with the following parameters:
|
||||
|
||||
- Client Type: `Confidential`
|
||||
- Response types: `code (Authorization Code Flow)`
|
||||
|
|
|
@ -10,30 +10,30 @@ From https://docs.ansible.com/ansible/2.5/reference_appendices/tower.html
|
|||
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, and AWX will be used interchangeably throughout this document.
|
||||
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
|
||||
- `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:
|
||||
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.
|
||||
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.
|
||||
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:
|
||||
Provide metadata in the `SAML Service Provider Organization Info` field:
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -45,7 +45,7 @@ Provide Metadata in the `SAML Service Provider Organization Info` Field:
|
|||
}
|
||||
```
|
||||
|
||||
Provide Metadata in the `SAML Service Provider Technical Contact` and `SAML Service Provider Technical Contact` Fields:
|
||||
Provide metadata in the `SAML Service Provider Technical Contact` and `SAML Service Provider Technical Contact` fields:
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -71,4 +71,4 @@ In the `SAML Enabled Identity Providers` paste the following configuration:
|
|||
}
|
||||
```
|
||||
|
||||
`x509cert` is the Certificate configured in passbook. Remove the --BEGIN CERTIFICATE-- and --END CERTIFICATE-- headers, then enter the cert as one non-breaking string.
|
||||
`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,33 +0,0 @@
|
|||
---
|
||||
apiVersion: apps/v1beta2
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: passbook-docs
|
||||
namespace: prod-passbook-docs
|
||||
labels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
app.kubernetes.io/managed-by: passbook-docs
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
spec:
|
||||
containers:
|
||||
- name: passbook-docs
|
||||
image: "beryju/passbook-docs:latest"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
resources:
|
||||
limits:
|
||||
cpu: 10m
|
||||
memory: 20Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 20Mi
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
name: passbook-docs
|
||||
namespace: prod-passbook-docs
|
||||
spec:
|
||||
rules:
|
||||
- host: docs.passbook.beryju.org
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: passbook-docs-http
|
||||
servicePort: http
|
||||
path: /
|
||||
tls:
|
||||
- hosts:
|
||||
- docs.passbook.beryju.org
|
||||
secretName: passbook-docs-acme
|
|
@ -1,17 +0,0 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: passbook-docs-http
|
||||
namespace: prod-passbook-docs
|
||||
labels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app.kubernetes.io/name: passbook-docs
|
30
docs/policies/expression.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
# 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.
|
||||
- `pb_flow_plan`: Current Plan if Policy is called from the Flow Planner.
|
|
@ -1,22 +0,0 @@
|
|||
# Expression Policy
|
||||
|
||||
Expression Policies allows you to write custom Policy Logic using Jinja2 Templating language.
|
||||
|
||||
For a language reference, see [here](https://jinja.palletsprojects.com/en/2.11.x/templates/).
|
||||
|
||||
The following objects are passed into the variable:
|
||||
|
||||
- `request`: A PolicyRequest object, which has the following properties:
|
||||
- `request.user`: The current User, which the Policy is applied against. ([ref](../../property-mappings/reference/user-object.md))
|
||||
- `request.http_request`: The Django HTTP Request, as documented [here](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.
|
||||
- `pb_flow_plan`: Current Plan if Policy is called while a flow is active.
|
||||
- `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external Provider.
|
||||
- `pb_is_group_member(user, group_name)`: Function which checks if `user` is member of a Group with Name `gorup_name`.
|
||||
- `pb_logger`: Standard Python Logger Object, which can be used to debug expressions.
|
||||
- `pb_client_ip`: Client's IP Address.
|
||||
|
||||
There are also the following custom filters available:
|
||||
|
||||
- `regex_match(regex)`: Return True if value matches `regex`
|
||||
- `regex_replace(regex, repl)`: Replace string matched by `regex` with `repl`
|
|
@ -2,29 +2,21 @@
|
|||
|
||||
## Kinds
|
||||
|
||||
There are two different Kind of policies, a Standard Policy and a Password Policy. Normal Policies just evaluate to True or False, and can be used everywhere. Password Policies apply when a Password is set (during User enrollment, Recovery or anywhere else). These policies can be used to apply Password Rules like length, etc. The can also be used to expire passwords after a certain amount of time.
|
||||
There are two different kinds of policies; Standard Policy and Password Policy. Normal policies evaluate to True or False, and can be used everywhere. Password policies apply when a password is set (during user enrollment, recovery or anywhere else). These policies can be used to apply password rules such as length, complexity, etc. They can also be used to expire passwords after a certain amount of time.
|
||||
|
||||
## Standard Policies
|
||||
|
||||
---
|
||||
|
||||
### Group-Membership Policy
|
||||
|
||||
This policy evaluates to True if the current user is a Member of the selected group.
|
||||
|
||||
### Reputation Policy
|
||||
|
||||
passbook keeps track of failed login attempts by Source IP and Attempted Username. These values are saved as scores. Each failed login decreases the Score for the Client IP as well as the targeted Username by one.
|
||||
passbook keeps track of failed login attempts by source IP and attempted username. These values are saved as scores. Each failed login decreases the score for the client IP as well as the targeted username by 1 (one).
|
||||
|
||||
This policy can be used to for example prompt Clients with a low score to pass a Captcha before they can continue.
|
||||
This policy can be used, for example, to prompt clients with a low score to pass a captcha before they can continue.
|
||||
|
||||
## Expression Policy
|
||||
|
||||
See [Expression Policy](expression/index.md).
|
||||
|
||||
### Webhook Policy
|
||||
|
||||
This policy allows you to send an arbitrary HTTP Request to any URL. You can then use JSONPath to extract the result you need.
|
||||
See [Expression Policy](expression.md).
|
||||
|
||||
## Password Policies
|
||||
|
||||
|
@ -32,19 +24,19 @@ This policy allows you to send an arbitrary HTTP Request to any URL. You can the
|
|||
|
||||
### Password Policy
|
||||
|
||||
This Policy allows you to specify Password rules, like Length and required Characters.
|
||||
This policy allows you to specify password rules, such as length and required characters.
|
||||
The following rules can be set:
|
||||
|
||||
- Minimum amount of Uppercase Characters
|
||||
- Minimum amount of Lowercase Characters
|
||||
- Minimum amount of Symbols Characters
|
||||
- Minimum Length
|
||||
- Symbol charset (define which characters are counted as symbols)
|
||||
- Minimum amount of uppercase characters.
|
||||
- Minimum amount of lowercase characters.
|
||||
- Minimum amount of symbols characters.
|
||||
- Minimum length.
|
||||
- Symbol charset (define which characters are counted as symbols).
|
||||
|
||||
### Have I Been Pwned Policy
|
||||
|
||||
This Policy checks the hashed Password against the [Have I Been Pwned](https://haveibeenpwned.com/) API. This only sends the first 5 characters of the hashed password. The remaining comparison is done within passbook.
|
||||
This policy checks the hashed password against the [Have I Been Pwned](https://haveibeenpwned.com/) API. This only sends the first 5 characters of the hashed password. The remaining comparison is done within passbook.
|
||||
|
||||
### Password-Expiry Policy
|
||||
|
||||
This policy can enforce regular password rotation by expiring set Passwords after a finite amount of time. This forces users to set a new password.
|
||||
This policy can enforce regular password rotation by expiring set passwords after a finite amount of time. This forces users to set a new password.
|
||||
|
|
12
docs/property-mappings/expression.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
# 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,21 +1,21 @@
|
|||
# Property Mappings
|
||||
|
||||
Property Mappings allow you to pass information to external Applications. For example, pass the current user's Groups as a SAML Parameter. Property Mappings are also used to map Source fields to passbook fields, for example when using LDAP.
|
||||
Property Mappings allow you to pass information to external applications. For example, pass the current user's groups as a SAML parameter. Property Mappings are also used to map Source fields to passbook fields, for example when using LDAP.
|
||||
|
||||
## SAML Property Mapping
|
||||
|
||||
SAML Property Mappings allow you embed Information into the SAML AuthN Request. THis Information can then be used by the Application to assign permissions for example.
|
||||
SAML Property Mappings allow you embed information into the SAML AuthN request. This information can then be used by the application to, for example, assign permissions to the object.
|
||||
|
||||
You can find examples [here](integrations/)
|
||||
You can find examples [here](integrations/).
|
||||
|
||||
## LDAP Property Mapping
|
||||
|
||||
LDAP Property Mappings are used when you define a LDAP Source. These Mappings define which LDAP Property maps to which passbook Property. By default, these mappings are created:
|
||||
LDAP Property Mappings are used when you define a LDAP Source. These mappings define which LDAP property maps to which passbook property. By default, the following mappings are created:
|
||||
|
||||
- Autogenerated LDAP Mapping: givenName -> first_name
|
||||
- Autogenerated LDAP Mapping: mail -> email
|
||||
- Autogenerated LDAP Mapping: name -> name
|
||||
- Autogenerated LDAP Mapping: sAMAccountName -> username
|
||||
- Autogenerated LDAP Mapping: sn -> last_name
|
||||
- Autogenerated LDAP Mapping: givenName -> first_name
|
||||
- Autogenerated LDAP Mapping: mail -> email
|
||||
- Autogenerated LDAP Mapping: name -> name
|
||||
- Autogenerated LDAP Mapping: sAMAccountName -> username
|
||||
- Autogenerated LDAP Mapping: sn -> last_name
|
||||
|
||||
These are configured for the most common LDAP Setups.
|
||||
These are configured with most common LDAP setups.
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
# Passbook User Object
|
||||
|
||||
The User object has the following attributes:
|
||||
|
||||
- `username`: User's Username
|
||||
- `email` User's E-Mail
|
||||
- `name` User's Display Name
|
||||
- `is_staff` Boolean field if user is staff
|
||||
- `is_active` Boolean field if user is active
|
||||
- `date_joined` Date User joined/was created
|
||||
- `password_change_date` Date Password was last changed
|
||||
- `attributes` Dynamic Attributes
|
||||
|
||||
## Examples
|
||||
|
||||
List all the User's Group Names
|
||||
|
||||
```jinja2
|
||||
[{% for group in user.groups.all() %}'{{ group.name }}',{% endfor %}]
|
||||
```
|
|
@ -1,17 +1,24 @@
|
|||
# Providers
|
||||
|
||||
Providers allow external Applications to authenticate against passbook and use its User Information.
|
||||
Providers allow external applications to authenticate against passbook and use its user information.
|
||||
|
||||
## OpenID Provider
|
||||
|
||||
This provider uses the commonly used OpenID Connect variation of OAuth2.
|
||||
This provider utilises the commonly used OpenID Connect variation of OAuth2.
|
||||
|
||||
## OAuth2 Provider
|
||||
|
||||
This provider is slightly different than the OpenID Provider. While it uses the same basic OAuth2 Protocol, it provides a GitHub-compatible Endpoint. This allows you to integrate Applications, which don't support Custom OpenID Providers.
|
||||
The API exposes Username, E-Mail, Name and Groups in a GitHub-compatible format.
|
||||
This provider is slightly different than the OpenID Provider. While it uses the same basic OAuth2 Protocol, it provides a GitHub-compatible endpoint. This allows you to integrate applications which don't support custom OpenID providers.
|
||||
The API exposes username, email, name, and groups in a GitHub-compatible format.
|
||||
This provider currently supports the following scopes:
|
||||
|
||||
- `openid`: Access OpenID Userinfo
|
||||
- `userinfo`: Access OpenID Userinfo
|
||||
- `email`: Access OpenID Email
|
||||
- `user:email`: GitHub Compatibility: User Email
|
||||
- `read:org`: GitHub Compatibility: User Groups
|
||||
|
||||
## SAML Provider
|
||||
|
||||
This provider allows you to integrate Enterprise Software using the SAML2 Protocol. It supports signed Requests. This Provider 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..."
|
||||
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".
|
||||
|
|
2
docs/requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
mkdocs
|
||||
mkdocs-material
|
1
docs/runtime.txt
Normal file
|
@ -0,0 +1 @@
|
|||
3.7
|
|
@ -1,39 +1,39 @@
|
|||
# Sources
|
||||
|
||||
Sources allow you to connect passbook to an existing User directory. They can also be used for Social-Login, using external Providers like Facebook, Twitter, etc.
|
||||
Sources allow you to connect passbook to an existing user directory. They can also be used for social logins, using external providers such as Facebook, Twitter, etc.
|
||||
|
||||
## Generic OAuth Source
|
||||
|
||||
**All Integration-specific Sources are documented in the Integrations Section**
|
||||
|
||||
This source allows users to enroll themselves with an External OAuth-based Identity Provider. The Generic Provider expects the Endpoint to return OpenID-Connect compatible Information. Vendor specific Implementations have their own OAuth Source.
|
||||
This source allows users to enroll themselves with an external OAuth-based Identity Provider. The generic provider expects the endpoint to return OpenID-Connect compatible information. Vendor-specific implementations have their own OAuth Source.
|
||||
|
||||
- Policies: Allow/Forbid Users from linking their Accounts with this Provider
|
||||
- Request Token URL: This field is used for OAuth v1 Implementations and will be provided by the Provider.
|
||||
- Authorization URL: This value will be provided by the Provider.
|
||||
- Access Token URL: This value will be provided by the Provider.
|
||||
- Profile URL: This URL is called by passbook to retrieve User information upon successful authentication.
|
||||
- Consumer key/Consumer secret: These values will be provided by the Provider.
|
||||
- Policies: Allow/Forbid users from linking their accounts with this provider.
|
||||
- Request Token URL: This field is used for OAuth v1 implementations and will be provided by the provider.
|
||||
- Authorization URL: This value will be provided by the provider.
|
||||
- Access Token URL: This value will be provided by the provider.
|
||||
- Profile URL: This URL is called by passbook to retrieve user information upon successful authentication.
|
||||
- Consumer key/Consumer secret: These values will be provided by the provider.
|
||||
|
||||
## SAML Source
|
||||
|
||||
This source allows passbook to act as a SAML Service Provider. Just like the SAML Provider, it supports signed Requests. Vendor specific documentation can be found in the Integrations Section
|
||||
This source allows passbook to act as a SAML Service Provider. Just like the SAML Provider, it supports signed requests. Vendor-specific documentation can be found in the Integrations Section.
|
||||
|
||||
## LDAP Source
|
||||
|
||||
This source allows you to import Users and Groups from an LDAP Server
|
||||
This source allows you to import users and groups from an LDAP Server.
|
||||
|
||||
- Server URI: URI to your LDAP Server/Domain Controller
|
||||
- Bind CN: CN to bind as, this can also be a UPN in the format of `user@domain.tld`
|
||||
- Bind password: Password used during the bind process
|
||||
- Enable Start TLS: Enables StartTLS functionality. To use SSL instead, use port `636`
|
||||
- Base DN: Base DN used for all LDAP queries
|
||||
- Addition User DN: Prepended to Base DN for User-queries.
|
||||
- Addition Group DN: Prepended to Base DN for Group-queries.
|
||||
- User object filter: Consider Objects matching this filter to be Users.
|
||||
- Group object filter: Consider Objects matching this filter to be Groups.
|
||||
- User group membership field: Field which contains Groups of user.
|
||||
- Object uniqueness field: Field which contains a unique Identifier.
|
||||
- Sync groups: Enable/disable Group synchronization. Groups are synced in the background every 5 minutes.
|
||||
- Sync parent group: Optionally set this Group as parent Group for all synced Groups (allows you to, for example, import AD Groups under a root `imported-from-ad` group.)
|
||||
- Property mappings: Define which LDAP Properties map to which passbook Properties. The default set of Property Mappings is generated for Active Directory. See also [LDAP Property Mappings](property-mappings/index.md#ldap-property-mapping)
|
||||
- Server URI: URI to your LDAP server/Domain Controller.
|
||||
- Bind CN: CN of the bind user. This can also be a UPN in the format of `user@domain.tld`.
|
||||
- Bind password: Password used during the bind process.
|
||||
- Enable StartTLS: Enables StartTLS functionality. To use LDAPS instead, use port `636`.
|
||||
- Base DN: Base DN used for all LDAP queries.
|
||||
- Addition User DN: Prepended to the base DN for user queries.
|
||||
- Addition Group DN: Prepended to the base DN for group queries.
|
||||
- User object filter: Consider objects matching this filter to be users.
|
||||
- Group object filter: Consider objects matching this filter to be groups.
|
||||
- User group membership field: This field contains the user's group memberships.
|
||||
- Object uniqueness field: This field contains a unique identifier.
|
||||
- Sync groups: Enable/disable group synchronization. Groups are synced in the background every 5 minutes.
|
||||
- Sync parent group: Optionally set this group as the parent group for all synced groups. An example use case of this would be to import Active Directory groups under a root `imported-from-ad` group.
|
||||
- Property mappings: Define which LDAP properties map to which passbook properties. The default set of property mappings is generated for Active Directory. See also [LDAP Property Mappings](property-mappings/index.md#ldap-property-mapping)
|
||||
|
|
27
docs/terminology.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
### Policy
|
||||
|
||||
At a base level a policy is a yes/no gate. It will either evaluate to True or False depending on the Policy Kind and settings. For example, a "Group Membership Policy" evaluates to True if the user is member of the specified Group and False if not. This can be used to conditionally apply Stages, grant/deny access to various objects, and for other custom logic.
|
||||
|
||||
### Provider
|
||||
|
||||
A Provider is a way for other applications to authenticate against passbook. Common Providers are OpenID Connect (OIDC) and SAML.
|
||||
|
||||
### Source
|
||||
|
||||
Sources are locations from which users can be added to passbook. For example, an LDAP Connection to import Users from Active Directory, or an OAuth2 Connection to allow Social Logins.
|
||||
|
||||
### Application
|
||||
|
||||
An application links together Policies with a Provider, allowing you to control access. It also holds Information like UI Name, Icon and more.
|
||||
|
||||
### Stages
|
||||
|
||||
A stage represents a single verification or logic step. They are used to authenticate users, enroll users, and more. These stages can optionally be applied to a flow via policies.
|
||||
|
||||
### Flows
|
||||
|
||||
Flows are an ordered sequence of stages. These flows can be used to define how a user authenticates, enrolls, etc.
|
||||
|
||||
### Property Mappings
|
||||
|
||||
Property Mappings allow you to make information available for external applications. For example, if you want to login to AWS with passbook, you'd use Property Mappings to set the user's roles in AWS based on their group memberships in passbook.
|
|
@ -4,13 +4,13 @@ Due to some database changes that had to be rather sooner than later, there is n
|
|||
|
||||
To export data from your old instance, run this command:
|
||||
|
||||
(with docker-compose)
|
||||
- docker-compose
|
||||
```
|
||||
docker-compose exec server ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
|
||||
docker cp passbook_server_1:/tmp/passbook_dump.json passbook_dump.json
|
||||
```
|
||||
|
||||
(with kubernetes)
|
||||
- kubernetes
|
||||
```
|
||||
kubectl exec -it passbook-web-... -- ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
|
||||
kubectl cp passbook-web-...:/tmp/passbook_dump.json passbook_dump.json
|
||||
|
@ -18,13 +18,13 @@ kubectl cp passbook-web-...:/tmp/passbook_dump.json passbook_dump.json
|
|||
|
||||
After that, create a new passbook instance in a different namespace (kubernetes) or in a different folder (docker-compose). Once this instance is running, you can use the following commands to restore the data. On docker-compose, you still have to run the `migrate` command, to create all database structures.
|
||||
|
||||
(docker-compose)
|
||||
- docker-compose
|
||||
```
|
||||
docker cp passbook_dump.json new_passbook_server_1:/tmp/passbook_dump.json
|
||||
docker-compose exec server ./manage.py loaddata /tmp/passbook_dump.json
|
||||
```
|
||||
|
||||
(with kubernetes)
|
||||
- kubernetes
|
||||
```
|
||||
kubectl cp passbook_dump.json passbook-web-...:/tmp/passbook_dump.json
|
||||
kubectl exec -it passbook-web-... -- ./manage.py loaddata /tmp/passbook_dump.json
|
||||
|
|
20
e2e/docker-compose.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
version: '3.7'
|
||||
|
||||
services:
|
||||
chrome:
|
||||
image: selenium/standalone-chrome-debug:3.141.59-20200525
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
network_mode: host
|
||||
|
||||
postgresql:
|
||||
image: postgres:11
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
POSTGRES_DB: passbook
|
||||
network_mode: host
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
network_mode: host
|
498
e2e/passbook.side
Normal file
|
@ -0,0 +1,498 @@
|
|||
{
|
||||
"id": "7d9b2407-1520-4c04-b040-68e8ada9aecc",
|
||||
"version": "2.0",
|
||||
"name": "passbook",
|
||||
"url": "http://localhost:8000",
|
||||
"tests": [{
|
||||
"id": "94b39863-74ec-4b7d-98c5-2b380b6d2c55",
|
||||
"name": "passbook login simple",
|
||||
"commands": [{
|
||||
"id": "e60e4382-4f96-44c3-ba06-5e18609c9c2b",
|
||||
"comment": "",
|
||||
"command": "open",
|
||||
"target": "/flows/default-authentication-flow/?next=%2F",
|
||||
"targets": [],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "b2652f24-931e-45b0-b01d-2f0ac0f74db8",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "id=id_uid_field",
|
||||
"targets": [
|
||||
["id=id_uid_field", "id"],
|
||||
["name=uid_field", "name"],
|
||||
["css=#id_uid_field", "css:finder"],
|
||||
["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "f1930f8a-984a-4076-a925-20937bb2f8d3",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_uid_field",
|
||||
"targets": [
|
||||
["id=id_uid_field", "id"],
|
||||
["name=uid_field", "name"],
|
||||
["css=#id_uid_field", "css:finder"],
|
||||
["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": "admin@example.tld"
|
||||
}, {
|
||||
"id": "0b568ee3-1bed-4821-a3bc-f6b960dbed9d",
|
||||
"comment": "",
|
||||
"command": "sendKeys",
|
||||
"target": "id=id_uid_field",
|
||||
"targets": [
|
||||
["id=id_uid_field", "id"],
|
||||
["name=uid_field", "name"],
|
||||
["css=#id_uid_field", "css:finder"],
|
||||
["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": "${KEY_ENTER}"
|
||||
}, {
|
||||
"id": "6d98e479-2825-484d-996a-ccf350d2761f",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_password",
|
||||
"targets": [
|
||||
["id=id_password", "id"],
|
||||
["name=password", "name"],
|
||||
["css=#id_password", "css:finder"],
|
||||
["xpath=//input[@id='id_password']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": "pbadmin"
|
||||
}, {
|
||||
"id": "6f7abec6-ff44-4eb5-ae23-520c1c29a706",
|
||||
"comment": "",
|
||||
"command": "sendKeys",
|
||||
"target": "id=id_password",
|
||||
"targets": [
|
||||
["id=id_password", "id"],
|
||||
["name=password", "name"],
|
||||
["css=#id_password", "css:finder"],
|
||||
["xpath=//input[@id='id_password']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": "${KEY_ENTER}"
|
||||
}, {
|
||||
"id": "04c5876f-1405-4077-a98b-e911f09113d7",
|
||||
"comment": "",
|
||||
"command": "assertText",
|
||||
"target": "xpath=//a[contains(@href, '/-/user/')]",
|
||||
"targets": [
|
||||
["linkText=pbadmin", "linkText"],
|
||||
["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
|
||||
["xpath=//a[contains(text(),'pbadmin')]", "xpath:link"],
|
||||
["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
|
||||
["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
|
||||
["xpath=//div[2]/a", "xpath:position"],
|
||||
["xpath=//a[contains(.,'pbadmin')]", "xpath:innerText"]
|
||||
],
|
||||
"value": "pbadmin"
|
||||
}]
|
||||
}, {
|
||||
"id": "61948b3c-3012-4f97-aa52-bc8f34fec333",
|
||||
"name": "passbook enroll simple",
|
||||
"commands": [{
|
||||
"id": "0f4884b3-4891-41bc-956d-1fa433e892e9",
|
||||
"comment": "",
|
||||
"command": "open",
|
||||
"target": "/flows/default-authentication-flow/?next=%2F",
|
||||
"targets": [],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "84d3861f-a60c-4650-8689-535f82b39577",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "linkText=Sign up.",
|
||||
"targets": [
|
||||
["linkText=Sign up.", "linkText"],
|
||||
["css=.pf-c-login__main-footer-band-item > a", "css:finder"],
|
||||
["xpath=//a[contains(text(),'Sign up.')]", "xpath:link"],
|
||||
["xpath=//main[@id='flow-body']/footer/div/p/a", "xpath:idRelative"],
|
||||
["xpath=//a[contains(@href, '/flows/default-enrollment-flow/')]", "xpath:href"],
|
||||
["xpath=//a", "xpath:position"],
|
||||
["xpath=//a[contains(.,'Sign up.')]", "xpath:innerText"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "a32435ca-d84a-41e7-a915-fcbbc5f88341",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_username",
|
||||
"targets": [
|
||||
["id=id_username", "id"],
|
||||
["name=username", "name"],
|
||||
["css=#id_username", "css:finder"],
|
||||
["xpath=//input[@id='id_username']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": "foo"
|
||||
}, {
|
||||
"id": "3b5dcf53-8297-46c5-88b7-11c2eb25f34f",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_password",
|
||||
"targets": [
|
||||
["id=id_password", "id"],
|
||||
["name=password", "name"],
|
||||
["css=#id_password", "css:finder"],
|
||||
["xpath=//input[@id='id_password']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": "pbadmin"
|
||||
}, {
|
||||
"id": "e948d61c-dae6-4994-b56f-ff130892b342",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_password_repeat",
|
||||
"targets": [
|
||||
["id=id_password_repeat", "id"],
|
||||
["name=password_repeat", "name"],
|
||||
["css=#id_password_repeat", "css:finder"],
|
||||
["xpath=//input[@id='id_password_repeat']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[3]/input", "xpath:idRelative"],
|
||||
["xpath=//div[3]/input", "xpath:position"]
|
||||
],
|
||||
"value": "pbadmin"
|
||||
}, {
|
||||
"id": "e7527bfc-ec74-4d96-86f0-5a3a55a59025",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "css=.pf-c-button",
|
||||
"targets": [
|
||||
["css=.pf-c-button", "css:finder"],
|
||||
["xpath=//button[@type='submit']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[4]/button", "xpath:idRelative"],
|
||||
["xpath=//button", "xpath:position"],
|
||||
["xpath=//button[contains(.,'Continue')]", "xpath:innerText"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "434b842c-a659-4ff5-aca8-06a6a3489597",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_name",
|
||||
"targets": [
|
||||
["id=id_name", "id"],
|
||||
["name=name", "name"],
|
||||
["css=#id_name", "css:finder"],
|
||||
["xpath=//input[@id='id_name']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": "some name"
|
||||
}, {
|
||||
"id": "cbc43a1b-2cfe-46e2-85bc-476fb32c6cb1",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_email",
|
||||
"targets": [
|
||||
["id=id_email", "id"],
|
||||
["name=email", "name"],
|
||||
["css=#id_email", "css:finder"],
|
||||
["xpath=//input[@id='id_email']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": "foo@bar.baz"
|
||||
}, {
|
||||
"id": "e74389a0-228b-4312-9677-e9add6358de3",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "css=.pf-c-button",
|
||||
"targets": [
|
||||
["css=.pf-c-button", "css:finder"],
|
||||
["xpath=//button[@type='submit']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[3]/button", "xpath:idRelative"],
|
||||
["xpath=//button", "xpath:position"],
|
||||
["xpath=//button[contains(.,'Continue')]", "xpath:innerText"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "3e22f9c2-5ebd-49c2-81b1-340fa0435bbc",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "linkText=foo",
|
||||
"targets": [
|
||||
["linkText=foo", "linkText"],
|
||||
["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
|
||||
["xpath=//a[contains(text(),'foo')]", "xpath:link"],
|
||||
["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
|
||||
["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
|
||||
["xpath=//div[2]/a", "xpath:position"],
|
||||
["xpath=//a[contains(.,'foo')]", "xpath:innerText"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "60124cfd-f11c-4d7f-8b01-bef54c8cbd73",
|
||||
"comment": "",
|
||||
"command": "assertText",
|
||||
"target": "xpath=//a[contains(@href, '/-/user/')]",
|
||||
"targets": [
|
||||
["linkText=foo", "linkText"],
|
||||
["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
|
||||
["xpath=//a[contains(text(),'foo')]", "xpath:link"],
|
||||
["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
|
||||
["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
|
||||
["xpath=//div[2]/a", "xpath:position"],
|
||||
["xpath=//a[contains(.,'foo')]", "xpath:innerText"]
|
||||
],
|
||||
"value": "foo"
|
||||
}, {
|
||||
"id": "429ee61b-9991-4919-8131-55f8e1bd9a0d",
|
||||
"comment": "",
|
||||
"command": "assertValue",
|
||||
"target": "id=id_username",
|
||||
"targets": [],
|
||||
"value": "foo"
|
||||
}, {
|
||||
"id": "f6c50760-52ed-4c1d-b232-30f8afe144eb",
|
||||
"comment": "",
|
||||
"command": "assertText",
|
||||
"target": "id=id_name",
|
||||
"targets": [
|
||||
["id=id_name", "id"],
|
||||
["name=name", "name"],
|
||||
["css=#id_name", "css:finder"],
|
||||
["xpath=//input[@id='id_name']", "xpath:attributes"],
|
||||
["xpath=//main[@id='main-content']/section/div/div/div/div[2]/form/div[2]/div/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/div/input", "xpath:position"]
|
||||
],
|
||||
"value": "some name"
|
||||
}, {
|
||||
"id": "b26905b5-89b5-4b41-abf5-a9f848f08622",
|
||||
"comment": "",
|
||||
"command": "assertText",
|
||||
"target": "id=id_email",
|
||||
"targets": [
|
||||
["id=id_email", "id"],
|
||||
["name=email", "name"],
|
||||
["css=#id_email", "css:finder"],
|
||||
["xpath=//input[@id='id_email']", "xpath:attributes"],
|
||||
["xpath=//main[@id='main-content']/section/div/div/div/div[2]/form/div[3]/div/input", "xpath:idRelative"],
|
||||
["xpath=//div[3]/div/input", "xpath:position"]
|
||||
],
|
||||
"value": "foo@bar.baz"
|
||||
}]
|
||||
}, {
|
||||
"id": "1a3172e0-ac23-4781-9367-19afccee4f4a",
|
||||
"name": "flows stage setup password",
|
||||
"commands": [{
|
||||
"id": "77784f77-d840-4b3d-a42f-7928f02fb7e1",
|
||||
"comment": "",
|
||||
"command": "open",
|
||||
"target": "/flows/default-authentication-flow/?next=%2F",
|
||||
"targets": [],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "783aa9a6-81e5-49c6-8789-2f360a5750b1",
|
||||
"comment": "",
|
||||
"command": "setWindowSize",
|
||||
"target": "1699x1417",
|
||||
"targets": [],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "cb0cd63e-30e9-4443-af59-5345fe26dc88",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "id=id_uid_field",
|
||||
"targets": [
|
||||
["id=id_uid_field", "id"],
|
||||
["name=uid_field", "name"],
|
||||
["css=#id_uid_field", "css:finder"],
|
||||
["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "8466ded1-c5f6-451c-b63f-0889da38503a",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_uid_field",
|
||||
"targets": [
|
||||
["id=id_uid_field", "id"],
|
||||
["name=uid_field", "name"],
|
||||
["css=#id_uid_field", "css:finder"],
|
||||
["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": "pbadmin"
|
||||
}, {
|
||||
"id": "27383093-d01a-4416-8fc6-9caad4926cd3",
|
||||
"comment": "",
|
||||
"command": "sendKeys",
|
||||
"target": "id=id_uid_field",
|
||||
"targets": [
|
||||
["id=id_uid_field", "id"],
|
||||
["name=uid_field", "name"],
|
||||
["css=#id_uid_field", "css:finder"],
|
||||
["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": "${KEY_ENTER}"
|
||||
}, {
|
||||
"id": "4602745a-0ebb-4425-a841-a1ed4899659d",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_password",
|
||||
"targets": [
|
||||
["id=id_password", "id"],
|
||||
["name=password", "name"],
|
||||
["css=#id_password", "css:finder"],
|
||||
["xpath=//input[@id='id_password']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": "pbadmin"
|
||||
}, {
|
||||
"id": "d1ff4f81-d8f9-45dc-ad5d-f99b54c0cd18",
|
||||
"comment": "",
|
||||
"command": "sendKeys",
|
||||
"target": "id=id_password",
|
||||
"targets": [
|
||||
["id=id_password", "id"],
|
||||
["name=password", "name"],
|
||||
["css=#id_password", "css:finder"],
|
||||
["xpath=//input[@id='id_password']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": "${KEY_ENTER}"
|
||||
}, {
|
||||
"id": "014c8f57-7ef2-469c-b700-efa94ba81b66",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "css=.pf-c-page__header",
|
||||
"targets": [
|
||||
["css=.pf-c-page__header", "css:finder"],
|
||||
["xpath=//div[@id='page-default-nav-example']/header", "xpath:idRelative"],
|
||||
["xpath=//header", "xpath:position"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "14e86b6f-6add-4bcc-913a-42b1e7322c79",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "linkText=pbadmin",
|
||||
"targets": [
|
||||
["linkText=pbadmin", "linkText"],
|
||||
["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
|
||||
["xpath=//a[contains(text(),'pbadmin')]", "xpath:link"],
|
||||
["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
|
||||
["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
|
||||
["xpath=//div[2]/a", "xpath:position"],
|
||||
["xpath=//a[contains(.,'pbadmin')]", "xpath:innerText"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "8280da13-632e-4cba-9e18-ecae0d57d052",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "linkText=Change password",
|
||||
"targets": [
|
||||
["linkText=Change password", "linkText"],
|
||||
["css=.pf-c-nav__section:nth-child(2) .pf-c-nav__link", "css:finder"],
|
||||
["xpath=//a[contains(text(),'Change password')]", "xpath:link"],
|
||||
["xpath=//nav[@id='page-default-nav-example-primary-nav']/section[2]/ul/li/a", "xpath:idRelative"],
|
||||
["xpath=//a[contains(@href, '/-/user/stage/password/b929b529-e384-4409-8d40-ac4a195fcab2/change/?next=%2F-%2Fuser%2F')]", "xpath:href"],
|
||||
["xpath=//section[2]/ul/li/a", "xpath:position"],
|
||||
["xpath=//a[contains(.,'Change password')]", "xpath:innerText"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "716d7e0c-79dc-469b-a31f-dceaa0765e9c",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "id=id_password",
|
||||
"targets": [
|
||||
["id=id_password", "id"],
|
||||
["name=password", "name"],
|
||||
["css=#id_password", "css:finder"],
|
||||
["xpath=//input[@id='id_password']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "77005d70-adf0-4add-8329-b092d43f829a",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_password",
|
||||
"targets": [
|
||||
["id=id_password", "id"],
|
||||
["name=password", "name"],
|
||||
["css=#id_password", "css:finder"],
|
||||
["xpath=//input[@id='id_password']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": "test"
|
||||
}, {
|
||||
"id": "965ca365-99f4-45d1-97c3-c944269341b9",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "id=id_password_repeat",
|
||||
"targets": [
|
||||
["id=id_password_repeat", "id"],
|
||||
["name=password_repeat", "name"],
|
||||
["css=#id_password_repeat", "css:finder"],
|
||||
["xpath=//input[@id='id_password_repeat']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "9b421468-c65e-4943-b6b1-1e80410a6b87",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_password_repeat",
|
||||
"targets": [
|
||||
["id=id_password_repeat", "id"],
|
||||
["name=password_repeat", "name"],
|
||||
["css=#id_password_repeat", "css:finder"],
|
||||
["xpath=//input[@id='id_password_repeat']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": "test"
|
||||
}, {
|
||||
"id": "572c1400-a0f2-499f-808a-18c1f56bf13f",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "css=.pf-c-button",
|
||||
"targets": [
|
||||
["css=.pf-c-button", "css:finder"],
|
||||
["xpath=//button[@type='submit']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[3]/button", "xpath:idRelative"],
|
||||
["xpath=//button", "xpath:position"],
|
||||
["xpath=//button[contains(.,'Continue')]", "xpath:innerText"]
|
||||
],
|
||||
"value": ""
|
||||
}]
|
||||
}],
|
||||
"suites": [{
|
||||
"id": "495657fb-3f5e-4431-877c-4d0b248c0841",
|
||||
"name": "Default Suite",
|
||||
"persistSession": false,
|
||||
"parallel": false,
|
||||
"timeout": 300,
|
||||
"tests": ["94b39863-74ec-4b7d-98c5-2b380b6d2c55"]
|
||||
}],
|
||||
"urls": ["http://localhost:8000/"],
|
||||
"plugins": []
|
||||
}
|
20
e2e/setup.sh
Executable file
|
@ -0,0 +1,20 @@
|
|||
#!/bin/bash -x
|
||||
# Setup docker & compose
|
||||
curl -fsSL https://get.docker.com | bash
|
||||
sudo usermod -a -G docker ubuntu
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.26.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
# Setup nodejs
|
||||
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
|
||||
# Setup docker
|
||||
sudo pip3 install pipenv
|
||||
|
||||
cd e2e
|
||||
sudo docker-compose up -d
|
||||
cd ..
|
||||
pipenv sync --dev
|
||||
pipenv shell
|
260
e2e/test_flows_enroll.py
Normal file
|
@ -0,0 +1,260 @@
|
|||
"""Test Enroll flow"""
|
||||
from time import sleep
|
||||
|
||||
from django.test import override_settings
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.stages.email.models import EmailStage, EmailTemplates
|
||||
from passbook.stages.identification.models import IdentificationStage
|
||||
from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||
from passbook.stages.user_login.models import UserLoginStage
|
||||
from passbook.stages.user_write.models import UserWriteStage
|
||||
|
||||
|
||||
class TestFlowsEnroll(SeleniumTestCase):
|
||||
"""Test Enroll flow"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.container = self.setup_client()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup test IdP container"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="mailhog/mailhog",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
healthcheck=Healthcheck(
|
||||
test=["CMD", "wget", "-s", "http://localhost:8025"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
|
||||
def test_enroll_2_step(self):
|
||||
"""Test 2-step enroll flow"""
|
||||
# First stage fields
|
||||
username_prompt = Prompt.objects.create(
|
||||
field_key="username", label="Username", order=0, type=FieldTypes.TEXT
|
||||
)
|
||||
password = Prompt.objects.create(
|
||||
field_key="password", label="Password", order=1, type=FieldTypes.PASSWORD
|
||||
)
|
||||
password_repeat = Prompt.objects.create(
|
||||
field_key="password_repeat",
|
||||
label="Password (repeat)",
|
||||
order=2,
|
||||
type=FieldTypes.PASSWORD,
|
||||
)
|
||||
|
||||
# Second stage fields
|
||||
name_field = Prompt.objects.create(
|
||||
field_key="name", label="Name", order=0, type=FieldTypes.TEXT
|
||||
)
|
||||
email = Prompt.objects.create(
|
||||
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.save()
|
||||
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
||||
second_stage.fields.set([name_field, email])
|
||||
second_stage.save()
|
||||
user_write = UserWriteStage.objects.create(name="enroll-user-write")
|
||||
user_login = UserLoginStage.objects.create(name="enroll-user-login")
|
||||
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name="policy-enrollment-password-equals",
|
||||
expression="return request.context['password'] == request.context['password_repeat']",
|
||||
)
|
||||
PolicyBinding.objects.create(
|
||||
target=first_stage, policy=password_policy, order=0
|
||||
)
|
||||
|
||||
flow = Flow.objects.create(
|
||||
name="default-enrollment-flow",
|
||||
slug="default-enrollment-flow",
|
||||
designation=FlowDesignation.ENROLLMENT,
|
||||
)
|
||||
|
||||
# Attach enrollment flow to identification stage
|
||||
ident_stage: IdentificationStage = IdentificationStage.objects.first()
|
||||
ident_stage.enrollment_flow = flow
|
||||
ident_stage.save()
|
||||
|
||||
FlowStageBinding.objects.create(flow=flow, stage=first_stage, order=0)
|
||||
FlowStageBinding.objects.create(flow=flow, stage=second_stage, order=1)
|
||||
FlowStageBinding.objects.create(flow=flow, stage=user_write, order=2)
|
||||
FlowStageBinding.objects.create(flow=flow, stage=user_login, order=3)
|
||||
|
||||
self.driver.get(self.live_server_url)
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[role=enroll]"))
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
||||
self.driver.find_element(By.ID, "id_username").send_keys("foo")
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
self.driver.find_element(By.ID, "id_name").send_keys("some name")
|
||||
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
|
||||
self.driver.find_element(By.LINK_TEXT, "foo").click()
|
||||
|
||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
|
||||
"some name",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||
"foo@bar.baz",
|
||||
)
|
||||
|
||||
@override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend")
|
||||
def test_enroll_email(self):
|
||||
"""Test enroll with Email verification"""
|
||||
# First stage fields
|
||||
username_prompt = Prompt.objects.create(
|
||||
field_key="username", label="Username", order=0, type=FieldTypes.TEXT
|
||||
)
|
||||
password = Prompt.objects.create(
|
||||
field_key="password", label="Password", order=1, type=FieldTypes.PASSWORD
|
||||
)
|
||||
password_repeat = Prompt.objects.create(
|
||||
field_key="password_repeat",
|
||||
label="Password (repeat)",
|
||||
order=2,
|
||||
type=FieldTypes.PASSWORD,
|
||||
)
|
||||
|
||||
# Second stage fields
|
||||
name_field = Prompt.objects.create(
|
||||
field_key="name", label="Name", order=0, type=FieldTypes.TEXT
|
||||
)
|
||||
email = Prompt.objects.create(
|
||||
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.save()
|
||||
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
||||
second_stage.fields.set([name_field, email])
|
||||
second_stage.save()
|
||||
email_stage = EmailStage.objects.create(
|
||||
name="enroll-email",
|
||||
host="localhost",
|
||||
port=1025,
|
||||
template=EmailTemplates.ACCOUNT_CONFIRM,
|
||||
)
|
||||
user_write = UserWriteStage.objects.create(name="enroll-user-write")
|
||||
user_login = UserLoginStage.objects.create(name="enroll-user-login")
|
||||
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name="policy-enrollment-password-equals",
|
||||
expression="return request.context['password'] == request.context['password_repeat']",
|
||||
)
|
||||
PolicyBinding.objects.create(
|
||||
target=first_stage, policy=password_policy, order=0
|
||||
)
|
||||
|
||||
flow = Flow.objects.create(
|
||||
name="default-enrollment-flow",
|
||||
slug="default-enrollment-flow",
|
||||
designation=FlowDesignation.ENROLLMENT,
|
||||
)
|
||||
|
||||
# Attach enrollment flow to identification stage
|
||||
ident_stage: IdentificationStage = IdentificationStage.objects.first()
|
||||
ident_stage.enrollment_flow = flow
|
||||
ident_stage.save()
|
||||
|
||||
FlowStageBinding.objects.create(flow=flow, stage=first_stage, order=0)
|
||||
FlowStageBinding.objects.create(flow=flow, stage=second_stage, order=1)
|
||||
FlowStageBinding.objects.create(flow=flow, stage=user_write, order=2)
|
||||
FlowStageBinding.objects.create(flow=flow, stage=email_stage, order=3)
|
||||
FlowStageBinding.objects.create(flow=flow, stage=user_login, order=4)
|
||||
|
||||
self.driver.get(self.live_server_url)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click()
|
||||
self.driver.find_element(By.ID, "id_username").send_keys("foo")
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
self.driver.find_element(By.ID, "id_name").send_keys("some name")
|
||||
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
sleep(3)
|
||||
|
||||
# Open Mailhog
|
||||
self.driver.get("http://localhost:8025")
|
||||
|
||||
# Click on first message
|
||||
self.driver.find_element(By.CLASS_NAME, "msglist-message").click()
|
||||
sleep(3)
|
||||
self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane"))
|
||||
self.driver.find_element(By.ID, "confirm").click()
|
||||
self.driver.close()
|
||||
self.driver.switch_to.window(self.driver.window_handles[0])
|
||||
|
||||
# We're now logged in
|
||||
sleep(3)
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
|
||||
"some name",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||
"foo@bar.baz",
|
||||
)
|
22
e2e/test_flows_login.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
"""test default login flow"""
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
|
||||
|
||||
class TestFlowsLogin(SeleniumTestCase):
|
||||
"""test default login flow"""
|
||||
|
||||
def test_login(self):
|
||||
"""test default login flow"""
|
||||
self.driver.get(f"{self.live_server_url}/flows/default-authentication-flow/")
|
||||
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.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
USER().username,
|
||||
)
|
41
e2e/test_flows_stage_setup.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
"""test stage setup flows (password change)"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
from time import sleep
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.core.models import User
|
||||
|
||||
|
||||
class TestFlowsStageSetup(SeleniumTestCase):
|
||||
"""test stage setup flows"""
|
||||
|
||||
def test_password_change(self):
|
||||
"""test password change flow"""
|
||||
new_password = "".join(
|
||||
SystemRandom().choice(string.ascii_uppercase + string.digits)
|
||||
for _ in range(8)
|
||||
)
|
||||
|
||||
self.driver.get(
|
||||
f"{self.live_server_url}/flows/default-authentication-flow/?next=%2F"
|
||||
)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
self.driver.find_element(By.LINK_TEXT, "Change password").click()
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(new_password)
|
||||
self.driver.find_element(By.ID, "id_password_repeat").click()
|
||||
self.driver.find_element(By.ID, "id_password_repeat").send_keys(new_password)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
|
||||
sleep(2)
|
||||
# Because USER() is cached, we need to get the user manually here
|
||||
user = User.objects.get(username=USER().username)
|
||||
self.assertTrue(user.check_password(new_password))
|
235
e2e/test_provider_oauth.py
Normal file
|
@ -0,0 +1,235 @@
|
|||
"""test OAuth Provider flow"""
|
||||
from time import sleep
|
||||
|
||||
from oauth2_provider.generators import generate_client_id, generate_client_secret
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.core.models import Application
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.providers.oauth.models import OAuth2Provider
|
||||
|
||||
|
||||
class TestProviderOAuth(SeleniumTestCase):
|
||||
"""test OAuth Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client_id = generate_client_id()
|
||||
self.client_secret = generate_client_secret()
|
||||
self.container = self.setup_client()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup client grafana container which we test OAuth against"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="grafana/grafana:latest",
|
||||
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_GITHUB_ENABLED": "true",
|
||||
"GF_AUTH_GITHUB_allow_sign_up": "true",
|
||||
"GF_AUTH_GITHUB_CLIENT_ID": self.client_id,
|
||||
"GF_AUTH_GITHUB_CLIENT_SECRET": self.client_secret,
|
||||
"GF_AUTH_GITHUB_SCOPES": "user:email,read:org",
|
||||
"GF_AUTH_GITHUB_AUTH_URL": self.url(
|
||||
"passbook_providers_oauth:github-authorize"
|
||||
),
|
||||
"GF_AUTH_GITHUB_TOKEN_URL": self.url(
|
||||
"passbook_providers_oauth:github-access-token"
|
||||
),
|
||||
"GF_AUTH_GITHUB_API_URL": self.url(
|
||||
"passbook_providers_oauth:github-user"
|
||||
),
|
||||
"GF_LOG_LEVEL": "debug",
|
||||
},
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
|
||||
def test_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"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type=OAuth2Provider.CLIENT_CONFIDENTIAL,
|
||||
authorization_grant_type=OAuth2Provider.GRANT_AUTHORIZATION_CODE,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
redirect_uris="http://localhost:3000/login/github",
|
||||
skip_authorization=True,
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--github").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
|
||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||
USER().username,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
|
||||
).get_attribute("value"),
|
||||
USER().username,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
|
||||
).get_attribute("value"),
|
||||
USER().username,
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type=OAuth2Provider.CLIENT_CONFIDENTIAL,
|
||||
authorization_grant_type=OAuth2Provider.GRANT_AUTHORIZATION_CODE,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
redirect_uris="http://localhost:3000/login/github",
|
||||
skip_authorization=True,
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--github").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
|
||||
self.assertIn(
|
||||
app.name,
|
||||
self.driver.find_element(
|
||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
|
||||
).text,
|
||||
)
|
||||
self.assertEqual(
|
||||
"GitHub Compatibility: User Email",
|
||||
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.CSS_SELECTOR, "[type=submit]").click()
|
||||
|
||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||
USER().username,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
|
||||
).get_attribute("value"),
|
||||
USER().username,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
|
||||
).get_attribute("value"),
|
||||
USER().username,
|
||||
)
|
||||
|
||||
def test_denied(self):
|
||||
"""test OAuth Provider flow (default authorization flow, denied)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type=OAuth2Provider.CLIENT_CONFIDENTIAL,
|
||||
authorization_grant_type=OAuth2Provider.GRANT_AUTHORIZATION_CODE,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
redirect_uris="http://localhost:3000/login/github",
|
||||
skip_authorization=True,
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
)
|
||||
|
||||
negative_policy = ExpressionPolicy.objects.create(
|
||||
name="negative-static", expression="return False"
|
||||
)
|
||||
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--github").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "#flow-body > header > h1").text,
|
||||
"Permission denied",
|
||||
)
|
303
e2e/test_provider_oidc.py
Normal file
|
@ -0,0 +1,303 @@
|
|||
"""test OpenID Provider flow"""
|
||||
from time import sleep
|
||||
|
||||
from django.shortcuts import reverse
|
||||
from oauth2_provider.generators import generate_client_id, generate_client_secret
|
||||
from oidc_provider.models import Client, ResponseType
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from e2e.utils import USER, SeleniumTestCase, ensure_rsa_key
|
||||
from passbook.core.models import Application
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.providers.oidc.models import OpenIDProvider
|
||||
|
||||
|
||||
class TestProviderOIDC(SeleniumTestCase):
|
||||
"""test OpenID Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client_id = generate_client_id()
|
||||
self.client_secret = generate_client_secret()
|
||||
self.container = self.setup_client()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup client grafana container which we test OIDC against"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="grafana/grafana:latest",
|
||||
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.live_server_url + reverse("passbook_providers_oidc:authorize")
|
||||
),
|
||||
"GF_AUTH_GENERIC_OAUTH_TOKEN_URL": (
|
||||
self.live_server_url + reverse("oidc_provider:token")
|
||||
),
|
||||
"GF_AUTH_GENERIC_OAUTH_API_URL": (
|
||||
self.live_server_url + reverse("oidc_provider:userinfo")
|
||||
),
|
||||
"GF_LOG_LEVEL": "debug",
|
||||
},
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
|
||||
def test_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"
|
||||
)
|
||||
client = Client.objects.create(
|
||||
name="grafana",
|
||||
client_type="confidential",
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
_redirect_uris="http://localhost:3000/",
|
||||
_scope="openid userinfo",
|
||||
)
|
||||
# At least one of these objects must exist
|
||||
ensure_rsa_key()
|
||||
# This response_code object might exist or not, depending on the order the tests are run
|
||||
rp_type, _ = ResponseType.objects.get_or_create(value="code")
|
||||
client.response_types.set([rp_type])
|
||||
client.save()
|
||||
provider = OpenIDProvider.objects.create(
|
||||
oidc_client=client, authorization_flow=authorization_flow,
|
||||
)
|
||||
Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
sleep(2)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "pf-c-title").text,
|
||||
"Redirect URI Error",
|
||||
)
|
||||
|
||||
def test_authorization_consent_implied(self):
|
||||
"""test OpenID Provider flow (default authorization flow with implied consent)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
client = Client.objects.create(
|
||||
name="grafana",
|
||||
client_type="confidential",
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
_redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
_scope="openid profile email",
|
||||
reuse_consent=False,
|
||||
require_consent=False,
|
||||
)
|
||||
# At least one of these objects must exist
|
||||
ensure_rsa_key()
|
||||
# This response_code object might exist or not, depending on the order the tests are run
|
||||
rp_type, _ = ResponseType.objects.get_or_create(value="code")
|
||||
client.response_types.set([rp_type])
|
||||
client.save()
|
||||
provider = OpenIDProvider.objects.create(
|
||||
oidc_client=client, authorization_flow=authorization_flow,
|
||||
)
|
||||
Application.objects.create(
|
||||
name="Grafana", slug="grafana", 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.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.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
|
||||
).get_attribute("value"),
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
client = Client.objects.create(
|
||||
name="grafana",
|
||||
client_type="confidential",
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
_redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
_scope="openid profile email",
|
||||
reuse_consent=False,
|
||||
require_consent=False,
|
||||
)
|
||||
# At least one of these objects must exist
|
||||
ensure_rsa_key()
|
||||
# This response_code object might exist or not, depending on the order the tests are run
|
||||
rp_type, _ = ResponseType.objects.get_or_create(value="code")
|
||||
client.response_types.set([rp_type])
|
||||
client.save()
|
||||
provider = OpenIDProvider.objects.create(
|
||||
oidc_client=client, authorization_flow=authorization_flow,
|
||||
)
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
|
||||
self.assertIn(
|
||||
app.name,
|
||||
self.driver.find_element(
|
||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
|
||||
).text,
|
||||
)
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
|
||||
)
|
||||
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.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
|
||||
).get_attribute("value"),
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
|
||||
def test_authorization_denied(self):
|
||||
"""test OpenID Provider flow (default authorization with access deny)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
)
|
||||
client = Client.objects.create(
|
||||
name="grafana",
|
||||
client_type="confidential",
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
_redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
_scope="openid profile email",
|
||||
reuse_consent=False,
|
||||
require_consent=False,
|
||||
)
|
||||
# At least one of these objects must exist
|
||||
ensure_rsa_key()
|
||||
# This response_code object might exist or not, depending on the order the tests are run
|
||||
rp_type, _ = ResponseType.objects.get_or_create(value="code")
|
||||
client.response_types.set([rp_type])
|
||||
client.save()
|
||||
provider = OpenIDProvider.objects.create(
|
||||
oidc_client=client, authorization_flow=authorization_flow,
|
||||
)
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
)
|
||||
|
||||
negative_policy = ExpressionPolicy.objects.create(
|
||||
name="negative-static", expression="return False"
|
||||
)
|
||||
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "#flow-body > header > h1").text,
|
||||
"Permission denied",
|
||||
)
|
216
e2e/test_provider_saml.py
Normal file
|
@ -0,0 +1,216 @@
|
|||
"""test SAML Provider flow"""
|
||||
from time import sleep
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.core.models import Application
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.lib.utils.reflection import class_to_path
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.providers.saml.models import (
|
||||
SAMLBindings,
|
||||
SAMLPropertyMapping,
|
||||
SAMLProvider,
|
||||
)
|
||||
from passbook.providers.saml.processors.generic import GenericProcessor
|
||||
|
||||
|
||||
class TestProviderSAML(SeleniumTestCase):
|
||||
"""test SAML Provider flow"""
|
||||
|
||||
container: Container
|
||||
|
||||
def setup_client(self, provider: SAMLProvider) -> Container:
|
||||
"""Setup client saml-sp container which we test SAML against"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="beryju/saml-test-sp",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
healthcheck=Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:9009/health"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
environment={
|
||||
"SP_ENTITY_ID": provider.issuer,
|
||||
"SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
||||
"SP_METADATA_URL": (
|
||||
self.url(
|
||||
"passbook_providers_saml:metadata",
|
||||
application_slug=provider.application.slug,
|
||||
)
|
||||
),
|
||||
},
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
|
||||
def test_sp_initiated_implicit(self):
|
||||
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
name="saml-test",
|
||||
processor_path=class_to_path(GenericProcessor),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="passbook-e2e",
|
||||
issuer="passbook-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
signing_kp=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="SAML", slug="passbook-saml", provider=provider,
|
||||
)
|
||||
self.container = self.setup_client(provider)
|
||||
self.driver.get("http://localhost:9009")
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "/html/body/pre").text,
|
||||
f"Hello, {USER().name}!",
|
||||
)
|
||||
|
||||
def test_sp_initiated_explicit(self):
|
||||
"""test SAML Provider flow SP-initiated flow (explicit consent)"""
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
)
|
||||
provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
name="saml-test",
|
||||
processor_path=class_to_path(GenericProcessor),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="passbook-e2e",
|
||||
issuer="passbook-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
signing_kp=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
name="SAML", slug="passbook-saml", provider=provider,
|
||||
)
|
||||
self.container = self.setup_client(provider)
|
||||
self.driver.get("http://localhost:9009")
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.assertIn(
|
||||
app.name,
|
||||
self.driver.find_element(
|
||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
|
||||
).text,
|
||||
)
|
||||
sleep(1)
|
||||
self.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}!",
|
||||
)
|
||||
|
||||
def test_idp_initiated_implicit(self):
|
||||
"""test SAML Provider flow IdP-initiated flow (implicit consent)"""
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
name="saml-test",
|
||||
processor_path=class_to_path(GenericProcessor),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="passbook-e2e",
|
||||
issuer="passbook-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
signing_kp=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="SAML", slug="passbook-saml", provider=provider,
|
||||
)
|
||||
self.container = self.setup_client(provider)
|
||||
self.driver.get(
|
||||
self.url(
|
||||
"passbook_providers_saml:sso-init",
|
||||
application_slug=provider.application.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.wait_for_url("http://localhost:9009/")
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "/html/body/pre").text,
|
||||
f"Hello, {USER().name}!",
|
||||
)
|
||||
|
||||
def test_sp_initiated_denied(self):
|
||||
"""test SAML Provider flow SP-initiated flow (Policy denies access)"""
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
negative_policy = ExpressionPolicy.objects.create(
|
||||
name="negative-static", expression="return False"
|
||||
)
|
||||
provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
name="saml-test",
|
||||
processor_path=class_to_path(GenericProcessor),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="passbook-e2e",
|
||||
issuer="passbook-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
signing_kp=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
name="SAML", slug="passbook-saml", provider=provider,
|
||||
)
|
||||
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
|
||||
self.container = self.setup_client(provider)
|
||||
self.driver.get("http://localhost:9009/")
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "#flow-body > header > h1").text,
|
||||
"Permission denied",
|
||||
)
|
127
e2e/test_source_saml.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
"""test SAML Source"""
|
||||
from time import sleep
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from e2e.utils import SeleniumTestCase
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.sources.saml.models import SAMLBindingTypes, SAMLSource
|
||||
|
||||
IDP_CERT = """-----BEGIN CERTIFICATE-----
|
||||
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBF
|
||||
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+Cgav
|
||||
Og8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+
|
||||
YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc
|
||||
+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyix
|
||||
YFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8
|
||||
jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/C
|
||||
YQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkw
|
||||
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6b
|
||||
lEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFs
|
||||
X1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7
|
||||
yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7
|
||||
NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG
|
||||
99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2n
|
||||
aQ==
|
||||
-----END CERTIFICATE-----"""
|
||||
|
||||
|
||||
class TestSourceSAML(SeleniumTestCase):
|
||||
"""test SAML Source flow"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.container = self.setup_client()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup test IdP container"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="kristophjunge/test-saml-idp",
|
||||
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={
|
||||
"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
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
|
||||
def test_idp_redirect(self):
|
||||
"""test SAML Source With redirect binding"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
keypair = CertificateKeyPair.objects.create(
|
||||
name="test-idp-cert", certificate_data=IDP_CERT
|
||||
)
|
||||
|
||||
SAMLSource.objects.create(
|
||||
name="saml-idp-test",
|
||||
slug="saml-idp-test",
|
||||
authentication_flow=authentication_flow,
|
||||
enrollment_flow=enrollment_flow,
|
||||
issuer="entity-id",
|
||||
sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
|
||||
binding_type=SAMLBindingTypes.Redirect,
|
||||
signing_kp=keypair,
|
||||
)
|
||||
|
||||
self.driver.get(self.live_server_url)
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the username field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
|
||||
self.driver.find_element(By.ID, "username").send_keys("user1")
|
||||
self.driver.find_element(By.ID, "password").send_keys("user1pass")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
||||
self.assertNotEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
||||
)
|
107
e2e/utils.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
"""passbook e2e testing utilities"""
|
||||
from functools import lru_cache
|
||||
from glob import glob
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from inspect import getmembers, isfunction
|
||||
from os import makedirs
|
||||
from time import time
|
||||
|
||||
from Cryptodome.PublicKey import RSA
|
||||
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 selenium import webdriver
|
||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
|
||||
|
||||
@lru_cache
|
||||
# pylint: disable=invalid-name
|
||||
def USER() -> User: # noqa
|
||||
"""Cached function that always returns pbadmin"""
|
||||
return User.objects.get(username="pbadmin")
|
||||
|
||||
|
||||
def ensure_rsa_key():
|
||||
"""Ensure that at least one RSAKey Object exists, create one if none exist"""
|
||||
from oidc_provider.models import RSAKey
|
||||
|
||||
if not RSAKey.objects.exists():
|
||||
key = RSA.generate(2048)
|
||||
rsakey = RSAKey(key=key.exportKey("PEM").decode("utf8"))
|
||||
rsakey.save()
|
||||
|
||||
|
||||
class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
makedirs("selenium_screenshots/", exist_ok=True)
|
||||
self.driver = self._get_driver()
|
||||
self.driver.maximize_window()
|
||||
self.driver.implicitly_wait(300)
|
||||
self.wait = WebDriverWait(self.driver, 500)
|
||||
self.apply_default_data()
|
||||
self.logger = get_logger()
|
||||
|
||||
def _get_driver(self) -> WebDriver:
|
||||
return webdriver.Remote(
|
||||
command_executor="http://localhost:4444/wd/hub",
|
||||
desired_capabilities=DesiredCapabilities.CHROME,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.driver.save_screenshot(
|
||||
f"selenium_screenshots/{self.__class__.__name__}_{time()}.png"
|
||||
)
|
||||
for line in self.driver.get_log("browser"):
|
||||
self.logger.warning(
|
||||
line["message"], source=line["source"], level=line["level"]
|
||||
)
|
||||
self.driver.quit()
|
||||
super().tearDown()
|
||||
|
||||
def wait_for_url(self, desired_url):
|
||||
"""Wait until URL is `desired_url`."""
|
||||
self.wait.until(
|
||||
lambda driver: driver.current_url == desired_url,
|
||||
f"URL {self.driver.current_url} doesn't match expected URL {desired_url}",
|
||||
)
|
||||
|
||||
def url(self, view, **kwargs) -> str:
|
||||
"""reverse `view` with `**kwargs` into full URL using live_server_url"""
|
||||
return self.live_server_url + reverse(view, kwargs=kwargs)
|
||||
|
||||
def apply_default_data(self):
|
||||
"""apply objects created by migrations after tables have been truncated"""
|
||||
# Find all migration files
|
||||
# load all functions
|
||||
migration_files = glob("**/migrations/*.py", recursive=True)
|
||||
matches = []
|
||||
for migration in migration_files:
|
||||
with open(migration, "r+") as migration_file:
|
||||
# Check if they have a `RunPython`
|
||||
if "RunPython" in migration_file.read():
|
||||
matches.append(migration)
|
||||
|
||||
with connection.schema_editor() as schema_editor:
|
||||
for match in matches:
|
||||
# Load module from file path
|
||||
spec = spec_from_file_location("", match)
|
||||
migration_module = module_from_spec(spec)
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
spec.loader.exec_module(migration_module)
|
||||
# Call all functions from module
|
||||
for _, func in getmembers(migration_module, isfunction):
|
||||
with transaction.atomic():
|
||||
try:
|
||||
func(apps, schema_editor)
|
||||
except IntegrityError:
|
||||
pass
|
|
@ -1,4 +1,4 @@
|
|||
FROM quay.io/pusher/oauth2_proxy
|
||||
FROM quay.io/oauth2-proxy/oauth2-proxy
|
||||
|
||||
COPY templates /templates
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
apiVersion: v1
|
||||
appVersion: "0.8.15-beta"
|
||||
appVersion: "0.9.0-pre4"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: "0.8.15-beta"
|
||||
version: "0.9.0-pre4"
|
||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
image:
|
||||
tag: 0.8.15-beta
|
||||
tag: 0.9.0-pre4
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"""Django manage.py"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from defusedxml import defuse_stdlib
|
||||
|
||||
defuse_stdlib()
|
||||
|
|
49
mkdocs.yml
|
@ -1,23 +1,43 @@
|
|||
site_name: passbook Docs
|
||||
site_url: https://beryju.github.io/passbook
|
||||
site_url: https://passbook.beryju.org/
|
||||
copyright: "Copyright © 2019 - 2020 BeryJu.org"
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Terminology: terminology.md
|
||||
- Installation:
|
||||
- Installation: installation/install.md
|
||||
- docker-compose: installation/docker-compose.md
|
||||
- Kubernetes: installation/kubernetes.md
|
||||
- Flows:
|
||||
Overview: flow/flows.md
|
||||
Examples:
|
||||
- Login: flow/examples/login.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: providers.md
|
||||
- Expressions:
|
||||
- Overview: expressions/index.md
|
||||
- Reference:
|
||||
- User Object: expressions/reference/user-object.md
|
||||
- Property Mappings:
|
||||
- Overview: property-mappings/index.md
|
||||
- Reference:
|
||||
- User Object: property-mappings/reference/user-object.md
|
||||
- Factors: factors.md
|
||||
- Expressions: property-mappings/expression.md
|
||||
- Policies:
|
||||
- Overview: policies/index.md
|
||||
- Expression: policies/expression/index.md
|
||||
- Expression: policies/expression.md
|
||||
- Integrations:
|
||||
- as Provider:
|
||||
- Amazon Web Services: integrations/services/aws/index.md
|
||||
|
@ -31,10 +51,23 @@ nav:
|
|||
repo_name: "BeryJu/passbook"
|
||||
repo_url: https://github.com/BeryJu/passbook
|
||||
theme:
|
||||
name: "material"
|
||||
logo: "images/logo.svg"
|
||||
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.8.15-beta"
|
||||
__version__ = "0.9.0-pre4"
|
||||
|
|
|
@ -1,4 +1,17 @@
|
|||
"""passbook core source form fields"""
|
||||
|
||||
SOURCE_FORM_FIELDS = ["name", "slug", "enabled"]
|
||||
SOURCE_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled"]
|
||||
SOURCE_FORM_FIELDS = [
|
||||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"authentication_flow",
|
||||
"enrollment_flow",
|
||||
]
|
||||
SOURCE_SERIALIZER_FIELDS = [
|
||||
"pk",
|
||||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"authentication_flow",
|
||||
"enrollment_flow",
|
||||
]
|
||||
|
|
|
@ -16,11 +16,13 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<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 class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<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>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
@ -65,14 +67,16 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Applications.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no applications exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Applications.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no applications exist. Click the button below to create one.' %}
|
||||
</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>
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<link rel="stylesheet" href="{% static 'node_modules/codemirror/theme/monokai.css' %}">
|
||||
<script src="{% static 'node_modules/codemirror/mode/xml/xml.js' %}"></script>
|
||||
<script src="{% static 'node_modules/codemirror/mode/yaml/yaml.js' %}"></script>
|
||||
<script src="{% static 'node_modules/codemirror/mode/jinja2/jinja2.js' %}"></script>
|
||||
<script src="{% static 'node_modules/codemirror/mode/python/python.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
|
@ -122,15 +122,21 @@
|
|||
{% trans 'Certificates' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:tokens' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:tokens' 'passbook_admin:token-delete' %}">
|
||||
{% trans 'Tokens' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:users' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:users' 'passbook_admin:user-create' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
|
||||
{% trans 'Users' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:groups' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:groups' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:groups' 'passbook_admin:group-create' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
|
||||
{% trans 'Groups' %}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -16,11 +16,13 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<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 class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<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>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
@ -67,14 +69,16 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes 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">
|
||||
{% trans 'Currently no certificates exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Certificates.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no certificates exist. Click the button below to create one.' %}
|
||||
</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>
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -16,16 +16,18 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<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>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<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>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</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 'Identifier' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Designation' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Stages' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Policies' %}</th>
|
||||
|
@ -37,8 +39,8 @@
|
|||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ flow.name }}</div>
|
||||
<small>{{ flow.slug }}</small>
|
||||
<div>{{ flow.slug }}</div>
|
||||
<small>{{ flow.name }}</small>
|
||||
</div>
|
||||
</th>
|
||||
<td role="cell">
|
||||
|
@ -59,6 +61,7 @@
|
|||
<td>
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-update' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:flow-delete' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-execute' pk=flow.pk %}?next={{ request.get_full_path }}">{% trans 'Execute' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -69,15 +72,16 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes 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">
|
||||
{% trans 'Currently no flows exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Flows.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no flows exist. Click the button below to create one.' %}
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -17,12 +17,14 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<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>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<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>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
@ -64,14 +66,16 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes 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">
|
||||
{% trans 'Currently no group exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Groups.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no group exist. Click the button below to create one.' %}
|
||||
</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>
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-l-gallery pf-m-gutter">
|
||||
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-applications"></i> {% trans 'Applications' %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -22,8 +22,8 @@
|
|||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<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>
|
||||
|
@ -33,8 +33,8 @@
|
|||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -49,8 +49,8 @@
|
|||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<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>
|
||||
|
@ -65,8 +65,8 @@
|
|||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<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>
|
||||
|
@ -76,8 +76,8 @@
|
|||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -92,8 +92,8 @@
|
|||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<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>
|
||||
|
@ -103,8 +103,8 @@
|
|||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -114,19 +114,29 @@
|
|||
</a>
|
||||
|
||||
<div class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ version }}
|
||||
{% if version >= version_latest %}
|
||||
<i class="pf-icon pf-icon-ok"></i>
|
||||
{% blocktrans with version=version %}
|
||||
{{ version }} (Up-to-date!)
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i>
|
||||
{% blocktrans with version=version latest=version_latest %}
|
||||
{{ version }} ({{ latest }} is available!)
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<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>
|
||||
|
@ -141,8 +151,8 @@
|
|||
</div>
|
||||
|
||||
<a class="pf-c-card pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<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>
|
||||
|
|
|
@ -16,28 +16,30 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<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:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<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:policy-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>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
@ -81,31 +83,33 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes 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">
|
||||
{% trans 'Currently no policies exist. Click the button below to create one.' %}
|
||||
</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:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Policies.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no policies exist. Click the button below to create one.' %}
|
||||
</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:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -16,12 +16,14 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:policy-binding-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:policy-binding-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
@ -57,14 +59,16 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Policy Bindings.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no policy bindings exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Policy Bindings.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no policy bindings exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:policy-binding-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:policy-binding-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -17,29 +17,31 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<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:property-mapping-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<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:property-mapping-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>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
@ -75,31 +77,33 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes 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">
|
||||
{% trans 'Currently no property mappings exist. Click the button below to create one.' %}
|
||||
</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:property-mapping-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Property Mappings.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no property mappings exist. Click the button below to create one.' %}
|
||||
</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:property-mapping-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -18,28 +18,30 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<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:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<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:provider-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>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
@ -94,30 +96,32 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes 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">
|
||||
{% trans 'Currently no providers exist. Click the button below to create one.' %}
|
||||
</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:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Providers.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no providers exist. Click the button below to create one.' %}
|
||||
</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:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -18,28 +18,30 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<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:source-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<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:source-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>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
@ -88,30 +90,32 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes 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">
|
||||
{% trans 'Currently no sources exist. Click the button below to create one.' %}
|
||||
</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:source-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Sources.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no sources exist. Click the button below to create one.' %}
|
||||
</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:source-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -18,28 +18,30 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<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:stage-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<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:stage-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>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
@ -62,6 +64,8 @@
|
|||
<ul>
|
||||
{% for flow in stage.flow_set.all %}
|
||||
<li><a href="{% url 'passbook_admin:flow-update' pk=flow.pk %}">{{ flow.slug }}</a></li>
|
||||
{% empty %}
|
||||
<li>-</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
|
@ -82,31 +86,33 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes 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">
|
||||
{% trans 'Currently no stages exist. Click the button below to create one.' %}
|
||||
</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:stage-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Stages.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no stages exist. Click the button below to create one.' %}
|
||||
</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:stage-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -16,12 +16,14 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:stage-binding-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:stage-binding-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
@ -37,8 +39,8 @@
|
|||
{% for flow in grouped_bindings %}
|
||||
<tr role="role">
|
||||
<td>
|
||||
{% blocktrans with name=flow.grouper.name %}
|
||||
Flow {{ name }}
|
||||
{% blocktrans with slug=flow.grouper.slug %}
|
||||
Flow {{ slug }}
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
<td></td>
|
||||
|
@ -54,9 +56,9 @@
|
|||
</td>
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ binding.flow.name }}</div>
|
||||
<div>{{ binding.flow.slug }}</div>
|
||||
<small>
|
||||
{{ binding.flow }}
|
||||
{{ binding.flow.name }}
|
||||
</small>
|
||||
</div>
|
||||
</th>
|
||||
|
@ -84,14 +86,16 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Flow-Stage Bindings.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no flow-stage bindings exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Flow-Stage Bindings.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no flow-stage bindings exist. Click the button below to create one.' %}
|
||||
</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>
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -17,12 +17,14 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<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>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<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>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
@ -57,14 +59,16 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes 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">
|
||||
{% trans 'Currently no invitations exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Invitations.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no invitations exist. Click the button below to create one.' %}
|
||||
</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>
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -17,11 +17,13 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<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 class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<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>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
@ -83,14 +85,16 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes 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">
|
||||
{% trans 'Currently no stage prompts exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Stage Prompts.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no stage prompts exist. Click the button below to create one.' %}
|
||||
</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>
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
83
passbook/admin/templates/administration/token/list.html
Normal file
|
@ -0,0 +1,83 @@
|
|||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="fas fa-key"></i>
|
||||
{% trans 'Tokens' %}
|
||||
</h1>
|
||||
<p>{% trans "Tokens are used throughout passbook for Email validation stages, Recovery keys and API access." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Token' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'User' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Expires?' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for token in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ token.pk }}</div>
|
||||
</div>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ token.user }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ token.expiring|yesno:"Yes,No" }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ token.expires }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:token-delete' pk=token.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Applications.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no applications exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -15,11 +15,13 @@
|
|||
<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" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
@ -64,14 +66,16 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Users.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no users exist. How did you even get here.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Users.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no users exist. How did you even get here.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|