diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 86c9f2dba..4f44b86b1 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.8.15-beta +current_version = 0.9.0-pre2 tag = True commit = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)\-(?P.*) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 07ba451a3..133f36b27 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: ["https://www.paypal.me/octocat"] +custom: ["https://www.paypal.me/beryju"] diff --git a/.github/workflows/ci-cleanup.yml b/.github/workflows/ci-cleanup.yml deleted file mode 100644 index 0d24155b0..000000000 --- a/.github/workflows/ci-cleanup.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: passbook-ci-cleanup -on: - - delete - -jobs: - delete-server: - runs-on: ubuntu-latest - steps: - - name: Delete docker tag - env: - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - run: curl - -u $DOCKER_USERNAME:$DOCKER_PASSWORD - -X "DELETE" - "https://hub.docker.com/v2/repositories/$DOCKER_USERNAME/passbook/tags/${GITHUB_REF##*/}/" - delete-gatekeeper: - runs-on: ubuntu-latest - steps: - - name: Delete docker tag - env: - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - run: curl - -u $DOCKER_USERNAME:$DOCKER_PASSWORD - -X "DELETE" - "https://hub.docker.com/v2/repositories/$DOCKER_USERNAME/passbook-gatekeeper/tags/${GITHUB_REF##*/}/" - delete-static: - runs-on: ubuntu-latest - steps: - - name: Delete docker tag - env: - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - run: curl - -u $DOCKER_USERNAME:$DOCKER_PASSWORD - -X "DELETE" - "https://hub.docker.com/v2/repositories/$DOCKER_USERNAME/passbook-static/tags/${GITHUB_REF##*/}/" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41a2c77e0..9289325d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -163,10 +163,10 @@ jobs: - name: Building Docker Image run: docker build --no-cache - -t beryju/passbook:${GITHUB_REF##*/} + -t beryju/passbook:gh-${GITHUB_REF##*/} -f Dockerfile . - name: Push Docker Container to Registry - run: docker push beryju/passbook:${GITHUB_REF##*/} + run: docker push beryju/passbook:gh-${GITHUB_REF##*/} build-gatekeeper: needs: - migrations @@ -184,10 +184,10 @@ jobs: cd gatekeeper docker build \ --no-cache \ - -t beryju/passbook-gatekeeper:${GITHUB_REF##*/} \ + -t beryju/passbook-gatekeeper:gh-${GITHUB_REF##*/} \ -f Dockerfile . - name: Push Docker Container to Registry - run: docker push beryju/passbook-gatekeeper:${GITHUB_REF##*/} + run: docker push beryju/passbook-gatekeeper:gh-${GITHUB_REF##*/} build-static: needs: - migrations @@ -213,7 +213,7 @@ jobs: run: docker build --no-cache --network=$(docker network ls | grep github | awk '{print $1}') - -t beryju/passbook-static:${GITHUB_REF##*/} + -t beryju/passbook-static:gh-${GITHUB_REF##*/} -f static.Dockerfile . - name: Push Docker Container to Registry - run: docker push beryju/passbook-static:${GITHUB_REF##*/} + run: docker push beryju/passbook-static:gh-${GITHUB_REF##*/} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63d80ac51..8a9a9ff7e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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-pre2 -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-pre2 - 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-pre2 \ -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-pre2 - 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-pre2 -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-pre2 - name: Push Docker Container to Registry (latest) run: docker push beryju/passbook-static:latest test-release: diff --git a/Pipfile.lock b/Pipfile.lock index e46301756..ede50ef06 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1243af11f030c4bb870928e3a36f9dcce7a170ce9c56689031ebe768de3ed3fa" + "sha256": "541f26a45f249fb2e61a597af7be7dee51eb8b40aa1035ae4081a455168128cc" }, "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,26 +46,26 @@ }, "boto3": { "hashes": [ - "sha256:bcaa88b2f81b88741c47da52f3414c876236700441df87b6198f860e6a200d6f", - "sha256:e974e7a3bbdbd6a73ffc07bea5fa0c0744a5a8b87dcca94702597176e3de465e" + "sha256:6a9cdab2db28330ffa3e6f08bb2bc07bc757d2019e4acf0c8376b72c63e7cc6b", + "sha256:f02c0c02f632285da124e560934145de64690000bf6348df8f1eb45239f0e9df" ], "index": "pypi", - "version": "==1.13.23" + "version": "==1.14.6" }, "botocore": { "hashes": [ - "sha256:17bc71415186efb86a25dd674f78064cdd85139485967d5a0741c7b83d62cf5b", - "sha256:e44b11b1c47c06b0f6524b0ff1fa1cae5ddea4eb06f359e4a9730e8e881b397a" + "sha256:a5737a5215f9db23344752a4d2a43646c104e6d500a2d6f9409624d2e58c92f1", + "sha256:c8b5143e2eaac20ce0d7238fd8ef33f7969139ccd616edb54fd3a482cdfd0e6c" ], - "version": "==1.16.24" + "version": "==1.17.6" }, "celery": { "hashes": [ - "sha256:9ae2e73b93cc7d6b48b56aaf49a68c91752d0ffd7dfdcc47f842ca79a6f13eae", - "sha256:c2037b6a8463da43b19969a0fc13f9023ceca6352b4dd51be01c66fbbb13647e" + "sha256:c3f4173f83ceb5a5c986c5fdaefb9456de3b0729a72a5776e46bd405fda7b647", + "sha256:d1762d6065522879f341c3d67c2b9fe4615eb79756d59acb1434601d4aca474b" ], "index": "pypi", - "version": "==4.4.4" + "version": "==4.4.5" }, "certifi": { "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:435e2ff5326e3218b6f7bde2f10253fe28b464d68d59421a7d1e895d2dc35ac6", + "sha256:8dbb8a84790f25b0b0da344c9381336fb207b431764d60cbcfa4a34940f2875d" ], "index": "pypi", - "version": "==0.9.1" + "version": "==0.9.2" }, "django-prometheus": { "hashes": [ - "sha256:1a8cb752ae4181e38df00e7bd7d5f6495cde18b8b3ff697c22f9d8d2fe48bf28", - "sha256:9f024af5495447c8e309f07e5289e7bc1100c5a380ac7cd0afe3a1b2a0b3b534" + "sha256:50e3c463f0c4f3310ba88ea19b4040f40ddd3c9ab1b5c1120b69c6021cf3a8d9", + "sha256:a61b0a187f86dba0cb168ee2ce0a0557f6bf5904187f9f120a5bccab9b52d141" ], "index": "pypi", - "version": "==2.1.0.dev14" + "version": "==2.1.0.dev32" }, "django-recaptcha": { "hashes": [ @@ -630,35 +623,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": [ @@ -694,10 +673,10 @@ }, "requests": { "hashes": [ - "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", - "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], - "version": "==2.23.0" + "version": "==2.24.0" }, "requests-oauthlib": { "hashes": [ @@ -748,11 +727,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": [ @@ -764,11 +743,11 @@ }, "signxml": { "hashes": [ - "sha256:2e186c117284fe5a0c543f5bcdde68f5a2341eeae219af9eb7e512dacf4bfce7", - "sha256:7d6af724542cae915bbb9000d333a52ce495d0b3cdcb4dc590c3c4a149b079ed" + "sha256:3b78b57e374d5517b4eddbbb27519208ccfbedbcabcf47ee322161f53482f174", + "sha256:e362b5eedbc785aec9ac1867f790ecf958d9640a95a34400a830350ba5bb1cc2" ], "index": "pypi", - "version": "==2.7.2" + "version": "==2.7.3" }, "six": { "hashes": [ @@ -794,10 +773,11 @@ }, "swagger-spec-validator": { "hashes": [ - "sha256:ba4e636d89bf547a6f41a6945bb503cf06dde13664ad978a7407aaed772ae75f" + "sha256:b651f881d718b0e3e867f19151bb47f7a50da611f285262f4d4aea092998347c", + "sha256:cb8a140c9c5d7d061d465416f156f432a92aa1a812b9c04f44e66c1568f13811" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.7.2" }, "uritemplate": { "hashes": [ @@ -836,17 +816,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": [ @@ -1116,11 +1096,11 @@ }, "pylint": { "hashes": [ - "sha256:b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c", - "sha256:dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b" + "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", + "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" ], "index": "pypi", - "version": "==2.5.2" + "version": "==2.5.3" }, "pylint-django": { "hashes": [ @@ -1170,37 +1150,29 @@ }, "regex": { "hashes": [ - "sha256:150125da109fccdcc8fec3b0b386b2a5d6ca7cff076f8b622486d1ca868b0c10", - "sha256:163bc0805e46acfa098dfc8c0b07f371577d505f603e48afc425ff475cdac3a5", - "sha256:20c513893ff80bdbe4b4ce11ea2e93d49481f05b270595d82af69ffc402010a6", - "sha256:21fc17cb868c4264f0813f992f46f9ae6fc8c309d4741091de4153bd1f6a6176", - "sha256:2c928bc8e0c453d73dffa3193a6e37ee752ea36df0dd4601e21024d98274dfad", - "sha256:2d9beca70e36f9c60d679e108c5fe49f3d4da79d13a13f91e5e759443bd954f9", - "sha256:5735f26cacdb50b3d6d35ebf8fdeb504bd8b381e2d079d2d9f12ce534fc14ecd", - "sha256:6edc5c190248d3b612f2cca45448cf8ebc3621d41afcd1c5708853cbb1dbb3b3", - "sha256:7606dba82435429641efe4fbc580574942f89cf2b9c5c1f8bc1eab2bacbf7e8b", - "sha256:8d1ee3796795e609ef7a3a5a35eaf4728038d986aa12c06b3fd1b92ee81911f4", - "sha256:8d9bb2d90e23c51aacbc58c1a11320f49b335cd67a91986cdbebcc3e843e4de8", - "sha256:97d414c41f19fd2362e493810caa8445c05e0a2d63a14081c972aad66284a8d2", - "sha256:9e37502817225ee99d91d8418f5119e98c380b00e772d06915690c05290f32ee", - "sha256:af7209b2fcc79ee2b0ad4ea080d70bb748450ec4f282cc9e864861e469b1072e", - "sha256:c0849b0864ff451f04c8afb5fc28e9ed592262e03debdd227cf0f53e04a55dcd", - "sha256:c4ac9215650688e78dea29b46adbdafb7b85058eebe92ef6ea848e14466c915f", - "sha256:dcda6d4e1bbfc939b177c237aee41c9678eaaf71df482688f8986e8251e12345", - "sha256:dd8501b8d9ea1aba53c4bc7d47bc72933f9b4213d534cf400f16c1431f51c8ba", - "sha256:ec0e509ed1877ff1cbc6f0864689bb60384a303502c4d72d9a635f8a4676fd3f", - "sha256:f6c8c3f56fef719180464855346e6e80971b86dfd9e5a0e356664b5baca53072", - "sha256:ffd4f80602490a309064cf2b203e220d581c51660e01055c64bf5da450485ee6" + "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.6.7" - }, - "selenium": { - "hashes": [ - "sha256:5f5489a0c5fe2f09cc6bc3f32a0d53441ab36882c987269f2afe805979633ac1", - "sha256:a9779ddc69cf03b75d94062c5e948f763919cf3341c77272f94cd05e6b4c7b32" - ], - "index": "pypi", - "version": "==4.0.0a6.post2" + "version": "==2020.6.8" }, "six": { "hashes": [ diff --git a/README.md b/README.md index 731732beb..33d7dd192 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# passbook +passbook logopassbook -![](https://img.shields.io/github/workflow/status/beryju/passbook/passbook-ci?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/github/workflow/status/beryju/passbook/passbook-ci?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 +28,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 diff --git a/docker-compose.yml b/docker-compose.yml index 95abb6381..a9a6aba8b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docs/Dockerfile b/docs/Dockerfile deleted file mode 100644 index d0953e867..000000000 --- a/docs/Dockerfile +++ /dev/null @@ -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 diff --git a/docs/build.sh b/docs/build.sh new file mode 100755 index 000000000..3ca164253 --- /dev/null +++ b/docs/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash -x +pip install -U mkdocs mkdocs-material +mkdocs gh-deploy diff --git a/docs/expressions/index.md b/docs/expressions/index.md index 4355dfca3..9f34050ef 100644 --- a/docs/expressions/index.md +++ b/docs/expressions/index.md @@ -1,6 +1,6 @@ # Expressions -Expressions allow you to write custom Logic using Python code. +Expressions allow you to write custom logic using Python code. Expressions are used in different places throughout passbook, and can do different things. @@ -46,7 +46,7 @@ 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. +Fetch a user matching `**filters`. Returns "None" if no user was found. Example: diff --git a/docs/expressions/reference/user-object.md b/docs/expressions/reference/user-object.md index 5cdb0780a..69eeeaaac 100644 --- a/docs/expressions/reference/user-object.md +++ b/docs/expressions/reference/user-object.md @@ -2,18 +2,18 @@ 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 + - `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 +List all the User's group names: ```python for group in user.groups.all(): diff --git a/docs/factors.md b/docs/factors.md deleted file mode 100644 index 8e55930c4..000000000 --- a/docs/factors.md +++ /dev/null @@ -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. diff --git a/docs/flow/examples/login.md b/docs/flow/examples/login.md new file mode 100644 index 000000000..1c141e8d1 --- /dev/null +++ b/docs/flow/examples/login.md @@ -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. diff --git a/docs/flow/examples/login.png b/docs/flow/examples/login.png new file mode 100644 index 000000000..5e5306d3b Binary files /dev/null and b/docs/flow/examples/login.png differ diff --git a/docs/flow/flows.md b/docs/flow/flows.md new file mode 100644 index 000000000..d8eec0690 --- /dev/null +++ b/docs/flow/flows.md @@ -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. diff --git a/docs/flow/stages/captcha/captcha-admin.png b/docs/flow/stages/captcha/captcha-admin.png new file mode 100644 index 000000000..f33a52972 Binary files /dev/null and b/docs/flow/stages/captcha/captcha-admin.png differ diff --git a/docs/flow/stages/captcha/index.md b/docs/flow/stages/captcha/index.md new file mode 100644 index 000000000..422caa4e4 --- /dev/null +++ b/docs/flow/stages/captcha/index.md @@ -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) diff --git a/docs/flow/stages/dummy/dummy.png b/docs/flow/stages/dummy/dummy.png new file mode 100644 index 000000000..7040e66cb Binary files /dev/null and b/docs/flow/stages/dummy/dummy.png differ diff --git a/docs/flow/stages/dummy/index.md b/docs/flow/stages/dummy/index.md new file mode 100644 index 000000000..9564585de --- /dev/null +++ b/docs/flow/stages/dummy/index.md @@ -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) diff --git a/docs/flow/stages/email/email-recovery.png b/docs/flow/stages/email/email-recovery.png new file mode 100644 index 000000000..26c0cccb8 Binary files /dev/null and b/docs/flow/stages/email/email-recovery.png differ diff --git a/docs/flow/stages/email/index.md b/docs/flow/stages/email/index.md new file mode 100644 index 000000000..75dcb6d57 --- /dev/null +++ b/docs/flow/stages/email/index.md @@ -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) diff --git a/docs/flow/stages/identification/index.md b/docs/flow/stages/identification/index.md new file mode 100644 index 000000000..c9b431e29 --- /dev/null +++ b/docs/flow/stages/identification/index.md @@ -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?`. diff --git a/docs/flow/stages/invitation/index.md b/docs/flow/stages/invitation/index.md new file mode 100644 index 000000000..06fd798ac --- /dev/null +++ b/docs/flow/stages/invitation/index.md @@ -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`. diff --git a/docs/flow/stages/otp/index.md b/docs/flow/stages/otp/index.md new file mode 100644 index 000000000..c14f6af27 --- /dev/null +++ b/docs/flow/stages/otp/index.md @@ -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. diff --git a/docs/flow/stages/password/index.md b/docs/flow/stages/password/index.md new file mode 100644 index 000000000..3c0aeeaed --- /dev/null +++ b/docs/flow/stages/password/index.md @@ -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. diff --git a/docs/flow/stages/prompt/index.md b/docs/flow/stages/prompt/index.md new file mode 100644 index 000000000..61b2f83d0 --- /dev/null +++ b/docs/flow/stages/prompt/index.md @@ -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('') +``` + +### `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. diff --git a/docs/flow/stages/prompt/validation.md b/docs/flow/stages/prompt/validation.md new file mode 100644 index 000000000..22d6b79f7 --- /dev/null +++ b/docs/flow/stages/prompt/validation.md @@ -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. diff --git a/docs/flow/stages/user_delete.md b/docs/flow/stages/user_delete.md new file mode 100644 index 000000000..040337a81 --- /dev/null +++ b/docs/flow/stages/user_delete.md @@ -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. diff --git a/docs/flow/stages/user_login.md b/docs/flow/stages/user_login.md new file mode 100644 index 000000000..ab3f18d6c --- /dev/null +++ b/docs/flow/stages/user_login.md @@ -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. diff --git a/docs/flow/stages/user_logout.md b/docs/flow/stages/user_logout.md new file mode 100644 index 000000000..47c7b3b7b --- /dev/null +++ b/docs/flow/stages/user_logout.md @@ -0,0 +1,3 @@ +# User Logout Stage + +Opposite stage of [User Login Stages](user_login.md). It removes the user from the current session. diff --git a/docs/flow/stages/user_write.md b/docs/flow/stages/user_write.md new file mode 100644 index 000000000..9fd718ce0 --- /dev/null +++ b/docs/flow/stages/user_write.md @@ -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. diff --git a/docs/images/brand.svg b/docs/images/brand.svg new file mode 100644 index 000000000..15c3f9d61 --- /dev/null +++ b/docs/images/brand.svg @@ -0,0 +1,2 @@ + diff --git a/docs/images/brand_inverted.svg b/docs/images/brand_inverted.svg new file mode 100644 index 000000000..86711c6bb --- /dev/null +++ b/docs/images/brand_inverted.svg @@ -0,0 +1,2 @@ + diff --git a/.github/screen_admin.png b/docs/images/screen_admin.png similarity index 100% rename from .github/screen_admin.png rename to docs/images/screen_admin.png diff --git a/.github/screen_apps.png b/docs/images/screen_apps.png similarity index 100% rename from .github/screen_apps.png rename to docs/images/screen_apps.png diff --git a/docs/index.md b/docs/index.md index 309acb089..954916e1d 100755 --- a/docs/index.md +++ b/docs/index.md @@ -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) diff --git a/docs/installation/docker-compose.md b/docs/installation/docker-compose.md index 4b34428ce..53ae3b333 100644 --- a/docs/installation/docker-compose.md +++ b/docs/installation/docker-compose.md @@ -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,10 +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. -The compose file references the current latest version, which can be overridden with the `SERVER_TAG` Environment variable. +``` +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 +``` -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. +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. 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. diff --git a/docs/installation/install.md b/docs/installation/install.md deleted file mode 100755 index ebd88c387..000000000 --- a/docs/installation/install.md +++ /dev/null @@ -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 diff --git a/docs/installation/kubernetes.md b/docs/installation/kubernetes.md index 1209bea09..35882adab 100644 --- a/docs/installation/kubernetes.md +++ b/docs/installation/kubernetes.md @@ -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. diff --git a/docs/integrations/services/aws/index.md b/docs/integrations/services/aws/index.md index af87c8d9d..f4835240f 100644 --- a/docs/integrations/services/aws/index.md +++ b/docs/integrations/services/aws/index.md @@ -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). diff --git a/docs/integrations/services/gitlab/index.md b/docs/integrations/services/gitlab/index.md index 7b04e5d83..2cd2bf47b 100644 --- a/docs/integrations/services/gitlab/index.md +++ b/docs/integrations/services/gitlab/index.md @@ -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 diff --git a/docs/integrations/services/harbor/index.md b/docs/integrations/services/harbor/index.md index fd1ac0142..4058a3e81 100644 --- a/docs/integrations/services/harbor/index.md +++ b/docs/integrations/services/harbor/index.md @@ -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)` diff --git a/docs/integrations/services/rancher/index.md b/docs/integrations/services/rancher/index.md index ccd4e0152..e2c9d8d51 100644 --- a/docs/integrations/services/rancher/index.md +++ b/docs/integrations/services/rancher/index.md @@ -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 diff --git a/docs/integrations/services/sentry/index.md b/docs/integrations/services/sentry/index.md index bb2456e36..d88de4fa7 100644 --- a/docs/integrations/services/sentry/index.md +++ b/docs/integrations/services/sentry/index.md @@ -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)` diff --git a/docs/integrations/services/tower-awx/index.md b/docs/integrations/services/tower-awx/index.md index 16c210e88..5855b8ab8 100644 --- a/docs/integrations/services/tower-awx/index.md +++ b/docs/integrations/services/tower-awx/index.md @@ -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. diff --git a/docs/k8s/deployment.yml b/docs/k8s/deployment.yml deleted file mode 100644 index 616245cbb..000000000 --- a/docs/k8s/deployment.yml +++ /dev/null @@ -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 diff --git a/docs/k8s/ingress.yml b/docs/k8s/ingress.yml deleted file mode 100644 index 210826cad..000000000 --- a/docs/k8s/ingress.yml +++ /dev/null @@ -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 diff --git a/docs/k8s/service.yml b/docs/k8s/service.yml deleted file mode 100644 index 0e83a1a8a..000000000 --- a/docs/k8s/service.yml +++ /dev/null @@ -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 diff --git a/docs/policies/expression.md b/docs/policies/expression.md index e3d0812cb..c057c9e41 100644 --- a/docs/policies/expression.md +++ b/docs/policies/expression.md @@ -1,5 +1,8 @@ # 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 @@ -18,10 +21,10 @@ return False ### Context variables - `request`: A PolicyRequest object, which has the following properties: - - `request.user`: The current User, which the Policy is applied against. ([ref](../expressions/reference/user-object.md)) + - `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.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_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. diff --git a/docs/policies/index.md b/docs/policies/index.md index 362f43d54..ad612c3e5 100644 --- a/docs/policies/index.md +++ b/docs/policies/index.md @@ -2,7 +2,7 @@ ## 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 @@ -10,9 +10,9 @@ There are two different Kind of policies, a Standard Policy and a Password Polic ### 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 @@ -24,19 +24,19 @@ See [Expression Policy](expression.md). ### 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. diff --git a/docs/property-mappings/expression.md b/docs/property-mappings/expression.md index a25ee2aad..067386b52 100644 --- a/docs/property-mappings/expression.md +++ b/docs/property-mappings/expression.md @@ -1,9 +1,12 @@ # Property Mapping Expressions -The property mapping should return a value that is expected by the Provider/Source. What types are supported, is documented in the individual Provider/Source. Returning `None` is always accepted, this simply skips this mapping. +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 might be `None` if there is no contextual user. ([ref](../expressions/reference/user-object.md)) -- `request`: The current request, this might be `None` if there is no contextual request. ([ref](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects)) -- Arbitrary other arguments given by the provider, this is documented on the Provider/Source. +- `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. diff --git a/docs/property-mappings/index.md b/docs/property-mappings/index.md index 7e7e21e17..7b7edd0d4 100644 --- a/docs/property-mappings/index.md +++ b/docs/property-mappings/index.md @@ -1,16 +1,16 @@ # 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 @@ -18,4 +18,4 @@ LDAP Property Mappings are used when you define a LDAP Source. These Mappings de - 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. diff --git a/docs/providers.md b/docs/providers.md index e7504e532..b48312612 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -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". diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..9a8a4ca47 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +mkdocs +mkdocs-material diff --git a/docs/runtime.txt b/docs/runtime.txt new file mode 100644 index 000000000..548d71365 --- /dev/null +++ b/docs/runtime.txt @@ -0,0 +1 @@ +3.7 \ No newline at end of file diff --git a/docs/sources.md b/docs/sources.md index 28c6e6bfd..d1ffab326 100644 --- a/docs/sources.md +++ b/docs/sources.md @@ -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) diff --git a/docs/terminology.md b/docs/terminology.md new file mode 100644 index 000000000..db3e72947 --- /dev/null +++ b/docs/terminology.md @@ -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. diff --git a/docs/upgrading-from-0.8.x.md b/docs/upgrading-from-0.8.x.md index 2e9e39d22..d0728f8db 100644 --- a/docs/upgrading-from-0.8.x.md +++ b/docs/upgrading-from-0.8.x.md @@ -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 diff --git a/gatekeeper/Dockerfile b/gatekeeper/Dockerfile index ff9e1c10a..85f0060f7 100644 --- a/gatekeeper/Dockerfile +++ b/gatekeeper/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/pusher/oauth2_proxy +FROM quay.io/oauth2-proxy/oauth2-proxy COPY templates /templates diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 18fedc7cf..5bf85f58d 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v1 -appVersion: "0.8.15-beta" +appVersion: "0.9.0-pre2" description: A Helm chart for passbook. name: passbook -version: "0.8.15-beta" +version: "0.9.0-pre2" icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png diff --git a/helm/values.yaml b/helm/values.yaml index 6e8c2c597..04ab6f169 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -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-pre2 nameOverride: "" diff --git a/mkdocs.yml b/mkdocs.yml index 2fd537455..5753f3a6f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,13 +1,31 @@ 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: @@ -17,7 +35,6 @@ nav: - Property Mappings: - Overview: property-mappings/index.md - Expressions: property-mappings/expression.md - - Factors: factors.md - Policies: - Overview: policies/index.md - Expression: policies/expression.md @@ -34,8 +51,12 @@ 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: @@ -46,6 +67,7 @@ markdown_extensions: smart_enable: all - pymdownx.inlinehilite - pymdownx.magiclink + - attr_list plugins: - search diff --git a/passbook/__init__.py b/passbook/__init__.py index 1cb087b69..6f59d3cef 100644 --- a/passbook/__init__.py +++ b/passbook/__init__.py @@ -1,2 +1,2 @@ """passbook""" -__version__ = "0.8.15-beta" +__version__ = "0.9.0-pre2" diff --git a/passbook/admin/templates/administration/application/list.html b/passbook/admin/templates/administration/application/list.html index 1e44f4e58..dca760042 100644 --- a/passbook/admin/templates/administration/application/list.html +++ b/passbook/admin/templates/administration/application/list.html @@ -16,11 +16,13 @@
{% if object_list %} -
-
- {% trans 'Create' %} +
+
+ + {% include 'partials/pagination.html' %}
- {% include 'partials/pagination.html' %}
@@ -65,14 +67,16 @@ {% else %}
- -

- {% trans 'No Applications.' %} -

-
- {% trans 'Currently no applications exist. Click the button below to create one.' %} +
+ +

+ {% trans 'No Applications.' %} +

+
+ {% trans 'Currently no applications exist. Click the button below to create one.' %} +
+ {% trans 'Create' %}
- {% trans 'Create' %}
{% endif %}
diff --git a/passbook/admin/templates/administration/audit/list.html b/passbook/admin/templates/administration/audit/list.html index ed793e650..408195272 100644 --- a/passbook/admin/templates/administration/audit/list.html +++ b/passbook/admin/templates/administration/audit/list.html @@ -15,8 +15,10 @@
-
- {% include 'partials/pagination.html' %} +
+
+ {% include 'partials/pagination.html' %} +
diff --git a/passbook/admin/templates/administration/certificatekeypair/list.html b/passbook/admin/templates/administration/certificatekeypair/list.html index 265547ca3..d009495dc 100644 --- a/passbook/admin/templates/administration/certificatekeypair/list.html +++ b/passbook/admin/templates/administration/certificatekeypair/list.html @@ -16,11 +16,13 @@
{% if object_list %} -
-
- {% trans 'Create' %} +
+
+ + {% include 'partials/pagination.html' %}
- {% include 'partials/pagination.html' %}
@@ -67,14 +69,16 @@ {% else %}
- -

- {% trans 'No Certificates.' %} -

-
- {% trans 'Currently no certificates exist. Click the button below to create one.' %} +
+ +

+ {% trans 'No Certificates.' %} +

+
+ {% trans 'Currently no certificates exist. Click the button below to create one.' %} +
+ {% trans 'Create' %}
- {% trans 'Create' %}
{% endif %}
diff --git a/passbook/admin/templates/administration/flow/list.html b/passbook/admin/templates/administration/flow/list.html index 9ee4e95c4..2e913d5d5 100644 --- a/passbook/admin/templates/administration/flow/list.html +++ b/passbook/admin/templates/administration/flow/list.html @@ -16,11 +16,13 @@
{% if object_list %} -
-
- {% trans 'Create' %} +
+
+ + {% include 'partials/pagination.html' %}
- {% include 'partials/pagination.html' %}
@@ -69,15 +71,16 @@ {% else %}
- -

- {% trans 'No Flows.' %} -

-
- {% trans 'Currently no flows exist. Click the button below to create one.' %} +
+ +

+ {% trans 'No Flows.' %} +

+
+ {% trans 'Currently no flows exist. Click the button below to create one.' %} +
+ {% trans 'Create' %}
- {% trans 'Create' %}
{% endif %}
diff --git a/passbook/admin/templates/administration/group/list.html b/passbook/admin/templates/administration/group/list.html index 77a80773a..c9ddbc929 100644 --- a/passbook/admin/templates/administration/group/list.html +++ b/passbook/admin/templates/administration/group/list.html @@ -17,12 +17,14 @@
{% if object_list %} -
-
- {% trans 'Create' %} +
+
+ + {% include 'partials/pagination.html' %}
- {% include 'partials/pagination.html' %}
@@ -64,14 +66,16 @@ {% else %}
- -

- {% trans 'No Groups.' %} -

-
- {% trans 'Currently no group exist. Click the button below to create one.' %} +
+ +

+ {% trans 'No Groups.' %} +

+
+ {% trans 'Currently no group exist. Click the button below to create one.' %} +
+ {% trans 'Create' %}
- {% trans 'Create' %}
{% endif %}
diff --git a/passbook/admin/templates/administration/overview.html b/passbook/admin/templates/administration/overview.html index 21dcb8906..bc586ce7c 100644 --- a/passbook/admin/templates/administration/overview.html +++ b/passbook/admin/templates/administration/overview.html @@ -11,8 +11,8 @@
@@ -81,31 +83,33 @@ {% else %}
- -

- {% trans 'No Policies.' %} -

-
- {% trans 'Currently no policies exist. Click the button below to create one.' %} -
-
- - +
+ +

+ {% trans 'No Policies.' %} +

+
+ {% trans 'Currently no policies exist. Click the button below to create one.' %} +
+
+ + +
{% endif %} diff --git a/passbook/admin/templates/administration/policybinding/list.html b/passbook/admin/templates/administration/policybinding/list.html index 173c56349..fc84753ae 100644 --- a/passbook/admin/templates/administration/policybinding/list.html +++ b/passbook/admin/templates/administration/policybinding/list.html @@ -16,12 +16,14 @@
{% if object_list %} -
-
- {% trans 'Create' %} +
+
+ + {% include 'partials/pagination.html' %}
- {% include 'partials/pagination.html' %}
@@ -57,14 +59,16 @@ {% else %}
- -

- {% trans 'No Policy Bindings.' %} -

-
- {% trans 'Currently no policy bindings exist. Click the button below to create one.' %} +
+ +

+ {% trans 'No Policy Bindings.' %} +

+
+ {% trans 'Currently no policy bindings exist. Click the button below to create one.' %} +
+ {% trans 'Create' %}
- {% trans 'Create' %}
{% endif %}
diff --git a/passbook/admin/templates/administration/property_mapping/list.html b/passbook/admin/templates/administration/property_mapping/list.html index 00d674870..0e3ea5314 100644 --- a/passbook/admin/templates/administration/property_mapping/list.html +++ b/passbook/admin/templates/administration/property_mapping/list.html @@ -17,29 +17,31 @@
{% if object_list %} -
-
-
- - +
+
+
+
+ + +
+ {% include 'partials/pagination.html' %}
- {% include 'partials/pagination.html' %}
@@ -75,31 +77,33 @@ {% else %}
- -

- {% trans 'No Property Mappings.' %} -

-
- {% trans 'Currently no property mappings exist. Click the button below to create one.' %} -
-
- - +
+ +

+ {% trans 'No Property Mappings.' %} +

+
+ {% trans 'Currently no property mappings exist. Click the button below to create one.' %} +
+
+ + +
{% endif %} diff --git a/passbook/admin/templates/administration/provider/list.html b/passbook/admin/templates/administration/provider/list.html index 5a804bfd6..95cc490e4 100644 --- a/passbook/admin/templates/administration/provider/list.html +++ b/passbook/admin/templates/administration/provider/list.html @@ -18,28 +18,30 @@
{% if object_list %} -
-
-
- - +
+
+
+
+ + +
+ {% include 'partials/pagination.html' %}
- {% include 'partials/pagination.html' %}
@@ -94,30 +96,32 @@ {% else %}
- -

- {% trans 'No Providers.' %} -

-
- {% trans 'Currently no providers exist. Click the button below to create one.' %} -
-
- - +
+ +

+ {% trans 'No Providers.' %} +

+
+ {% trans 'Currently no providers exist. Click the button below to create one.' %} +
+
+ + +
{% endif %} diff --git a/passbook/admin/templates/administration/source/list.html b/passbook/admin/templates/administration/source/list.html index 067a6501e..7ce88965a 100644 --- a/passbook/admin/templates/administration/source/list.html +++ b/passbook/admin/templates/administration/source/list.html @@ -18,28 +18,30 @@
{% if object_list %} -
-
-
- - +
+
+
+
+ + +
+ {% include 'partials/pagination.html' %}
- {% include 'partials/pagination.html' %}
@@ -88,30 +90,32 @@ {% else %}
- -

- {% trans 'No Sources.' %} -

-
- {% trans 'Currently no sources exist. Click the button below to create one.' %} -
-
- - +
+ +

+ {% trans 'No Sources.' %} +

+
+ {% trans 'Currently no sources exist. Click the button below to create one.' %} +
+
+ + +
{% endif %} diff --git a/passbook/admin/templates/administration/stage/list.html b/passbook/admin/templates/administration/stage/list.html index 624f5374e..8f92ebdcc 100644 --- a/passbook/admin/templates/administration/stage/list.html +++ b/passbook/admin/templates/administration/stage/list.html @@ -18,28 +18,30 @@
{% if object_list %} -
-
-
- - +
+
+
+
+ + +
+ {% include 'partials/pagination.html' %}
- {% include 'partials/pagination.html' %}
@@ -62,6 +64,8 @@
    {% for flow in stage.flow_set.all %}
  • {{ flow.slug }}
  • + {% empty %} +
  • -
  • {% endfor %}
@@ -82,31 +86,33 @@ {% else %}
- -

- {% trans 'No Stages.' %} -

-
- {% trans 'Currently no stages exist. Click the button below to create one.' %} -
-
- - +
+ +

+ {% trans 'No Stages.' %} +

+
+ {% trans 'Currently no stages exist. Click the button below to create one.' %} +
+
+ + +
{% endif %} diff --git a/passbook/admin/templates/administration/stage_binding/list.html b/passbook/admin/templates/administration/stage_binding/list.html index 7c1d94f79..efdf7f235 100644 --- a/passbook/admin/templates/administration/stage_binding/list.html +++ b/passbook/admin/templates/administration/stage_binding/list.html @@ -16,12 +16,14 @@
{% if object_list %} -
-
- {% trans 'Create' %} +
+
+ + {% include 'partials/pagination.html' %}
- {% include 'partials/pagination.html' %}
@@ -84,14 +86,16 @@ {% else %}
- -

- {% trans 'No Flow-Stage Bindings.' %} -

-
- {% trans 'Currently no flow-stage bindings exist. Click the button below to create one.' %} +
+ +

+ {% trans 'No Flow-Stage Bindings.' %} +

+
+ {% trans 'Currently no flow-stage bindings exist. Click the button below to create one.' %} +
+ {% trans 'Create' %}
- {% trans 'Create' %}
{% endif %}
diff --git a/passbook/admin/templates/administration/stage_invitation/list.html b/passbook/admin/templates/administration/stage_invitation/list.html index 779e1344d..e341cae82 100644 --- a/passbook/admin/templates/administration/stage_invitation/list.html +++ b/passbook/admin/templates/administration/stage_invitation/list.html @@ -17,12 +17,14 @@
{% if object_list %} -
-
- {% trans 'Create' %} +
+
+ + {% include 'partials/pagination.html' %}
- {% include 'partials/pagination.html' %}
@@ -57,14 +59,16 @@ {% else %}
- -

- {% trans 'No Invitations.' %} -

-
- {% trans 'Currently no invitations exist. Click the button below to create one.' %} +
+ +

+ {% trans 'No Invitations.' %} +

+
+ {% trans 'Currently no invitations exist. Click the button below to create one.' %} +
+ {% trans 'Create' %}
- {% trans 'Create' %}
{% endif %}
diff --git a/passbook/admin/templates/administration/stage_prompt/list.html b/passbook/admin/templates/administration/stage_prompt/list.html index 5aff63c69..7b9503944 100644 --- a/passbook/admin/templates/administration/stage_prompt/list.html +++ b/passbook/admin/templates/administration/stage_prompt/list.html @@ -17,11 +17,13 @@
{% if object_list %} -
-
- {% trans 'Create' %} +
+
+ + {% include 'partials/pagination.html' %}
- {% include 'partials/pagination.html' %}
@@ -83,14 +85,16 @@ {% else %}
- -

- {% trans 'No Stage Prompts.' %} -

-
- {% trans 'Currently no stage prompts exist. Click the button below to create one.' %} +
+ +

+ {% trans 'No Stage Prompts.' %} +

+
+ {% trans 'Currently no stage prompts exist. Click the button below to create one.' %} +
+ {% trans 'Create' %}
- {% trans 'Create' %}
{% endif %}
diff --git a/passbook/admin/templates/administration/user/list.html b/passbook/admin/templates/administration/user/list.html index 8293b769f..770d903cd 100644 --- a/passbook/admin/templates/administration/user/list.html +++ b/passbook/admin/templates/administration/user/list.html @@ -15,11 +15,13 @@
{% if object_list %} -
-
- {% trans 'Create' %} +
+
+ + {% include 'partials/pagination.html' %}
- {% include 'partials/pagination.html' %}
@@ -64,14 +66,16 @@ {% else %}
- -

- {% trans 'No Users.' %} -

-
- {% trans 'Currently no users exist. How did you even get here.' %} +
+ +

+ {% trans 'No Users.' %} +

+
+ {% trans 'Currently no users exist. How did you even get here.' %} +
+ {% trans 'Create' %}
- {% trans 'Create' %}
{% endif %}
diff --git a/passbook/admin/templates/generic/form.html b/passbook/admin/templates/generic/form.html index 1d535b5dd..44f8f0e83 100644 --- a/passbook/admin/templates/generic/form.html +++ b/passbook/admin/templates/generic/form.html @@ -35,10 +35,12 @@ {% block beneath_form %} {% endblock %}
-
- diff --git a/passbook/admin/templates/generic/update.html b/passbook/admin/templates/generic/update.html index 7c7f57290..cee51661c 100644 --- a/passbook/admin/templates/generic/update.html +++ b/passbook/admin/templates/generic/update.html @@ -6,7 +6,7 @@ {% block above_form %}

{% blocktrans with type=form|form_verbose_name|title inst=form.instance %} - Update {{ type }}: {{ inst }} + Update {{ inst }} {% endblocktrans %}

{% endblock %} diff --git a/passbook/core/migrations/0003_default_user.py b/passbook/core/migrations/0003_default_user.py index e1d52f3b4..d236c8f90 100644 --- a/passbook/core/migrations/0003_default_user.py +++ b/passbook/core/migrations/0003_default_user.py @@ -6,10 +6,10 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): - # User = apps.get_model("passbook_core", "User") + # We have to use a direct import here, otherwise we get an object manager error from passbook.core.models import User - pbadmin = User.objects.create( + pbadmin, _ = User.objects.get_or_create( username="pbadmin", email="root@localhost", name="passbook Default Admin" ) pbadmin.set_password("pbadmin") # noqa # nosec diff --git a/passbook/core/templates/base/page.html b/passbook/core/templates/base/page.html index a7bdcefd5..2a25d2565 100644 --- a/passbook/core/templates/base/page.html +++ b/passbook/core/templates/base/page.html @@ -25,8 +25,8 @@
-
{% else %} -
- -

{% trans 'No Applications available.' %}

-
- {% trans "Either no applications are defined, or you don't have access to any." %} +
+
+ +

{% trans 'No Applications available.' %}

+
+ {% trans "Either no applications are defined, or you don't have access to any." %} +
+ {% if user.is_superuser %} {# todo: use guardian permissions instead #} + + {% trans 'Create Application' %} + + {% endif %}
- {% if user.is_superuser %} {# todo: use guardian permissions instead #} - - {% trans 'Create Application' %} - - {% endif %}
{% endif %} diff --git a/passbook/core/templates/partials/form.html b/passbook/core/templates/partials/form.html index efd249d08..c954ecb18 100644 --- a/passbook/core/templates/partials/form.html +++ b/passbook/core/templates/partials/form.html @@ -10,6 +10,9 @@
{% endif %} {% for field in form %} +{% if field.field.widget|fieldtype == 'HiddenInput' %} + {{ field }} +{% else %}
{% if field.field.widget|fieldtype == 'RadioSelect' %}
+{% endif %} {% endfor %} diff --git a/passbook/core/templates/partials/form_horizontal.html b/passbook/core/templates/partials/form_horizontal.html index 1e2836e6e..e717fa442 100644 --- a/passbook/core/templates/partials/form_horizontal.html +++ b/passbook/core/templates/partials/form_horizontal.html @@ -5,55 +5,72 @@ {% for field in form %}
{% if field.field.widget|fieldtype == 'RadioSelect' %} - - {% for c in field %} -
- - +
+
- {% endfor %} - {% elif field.field.widget|fieldtype == 'Select' %} - -
- {{ field|css_class:"pf-c-form-control" }} - {% if field.help_text %} -

{{ field.help_text|safe }}

- {% endif %} -
- {% elif field.field.widget|fieldtype == 'CheckboxInput' %} -
-
- {{ field|css_class:"pf-c-check__input" }} - +
+ {% for c in field %} +
+ +
{% if field.help_text %} -

{{ field.help_text|safe }}

+

{{ field.help_text }}

{% endif %} + {% endfor %} +
+ {% elif field.field.widget|fieldtype == 'Select' %} +
+ +
+
+
+ {{ field|css_class:"pf-c-form-control" }} + {% if field.help_text %} +

{{ field.help_text|safe }}

+ {% endif %} +
+
+ {% elif field.field.widget|fieldtype == 'CheckboxInput' %} +
+
+
+ {{ field|css_class:"pf-c-check__input" }} + +
+ {% if field.help_text %} +

{{ field.help_text|safe }}

+ {% endif %} +
{% else %} - -
- {{ field|css_class:'pf-c-form-control' }} - {% if field.help_text %} -

{{ field.help_text|safe }}

- {% endif %} +
+ +
+
+
+ {{ field|css_class:'pf-c-form-control' }} + {% if field.help_text %} +

{{ field.help_text|safe }}

+ {% endif %} +
{% endif %} {% for error in field.errors %} diff --git a/passbook/core/templates/partials/pagination.html b/passbook/core/templates/partials/pagination.html index 9fbe8c855..c026e558c 100644 --- a/passbook/core/templates/partials/pagination.html +++ b/passbook/core/templates/partials/pagination.html @@ -1,43 +1,43 @@ {% load i18n %} -
-
- {{ page_obj.start_index }} - {{ page_obj.end_index }}of - {{ page_obj.count }} -
- {% with param=get_param|default:'page' %} - - {% endwith %} + {% with param=get_param|default:'page' %} + + {% endwith %} +
diff --git a/passbook/flows/forms.py b/passbook/flows/forms.py index 6f695050f..8c8d5bdc6 100644 --- a/passbook/flows/forms.py +++ b/passbook/flows/forms.py @@ -44,6 +44,9 @@ class FlowStageBindingForm(forms.ModelForm): "re_evaluate_policies", "order", ] + labels = { + "re_evaluate_policies": _("Re-evaluate Policies"), + } widgets = { "name": forms.TextInput(), } diff --git a/passbook/flows/markers.py b/passbook/flows/markers.py new file mode 100644 index 000000000..628f0cf28 --- /dev/null +++ b/passbook/flows/markers.py @@ -0,0 +1,50 @@ +"""Stage Markers""" +from dataclasses import dataclass +from typing import TYPE_CHECKING, Optional + +from structlog import get_logger + +from passbook.core.models import User +from passbook.flows.models import Stage +from passbook.policies.engine import PolicyEngine +from passbook.policies.models import PolicyBinding + +if TYPE_CHECKING: + from passbook.flows.planner import FlowPlan + +LOGGER = get_logger() + + +@dataclass +class StageMarker: + """Base stage marker class, no extra attributes, and has no special handler.""" + + # pylint: disable=unused-argument + def process(self, plan: "FlowPlan", stage: Stage) -> Optional[Stage]: + """Process callback for this marker. This should be overridden by sub-classes. + If a stage should be removed, return None.""" + return stage + + +@dataclass +class ReevaluateMarker(StageMarker): + """Reevaluate Marker, forces stage's policies to be evaluated again.""" + + binding: PolicyBinding + user: User + + def process(self, plan: "FlowPlan", stage: Stage) -> Optional[Stage]: + """Re-evaluate policies bound to stage, and if they fail, remove from plan""" + engine = PolicyEngine(self.binding, self.user) + engine.use_cache = False + engine.request.context = plan.context + engine.build() + result = engine.result + if result.passing: + return stage + LOGGER.warning( + "f(plan_inst)[re-eval marker]: stage failed re-evaluation", + stage=stage, + messages=result.messages, + ) + return None diff --git a/passbook/flows/planner.py b/passbook/flows/planner.py index ca8a81c5e..86acdb630 100644 --- a/passbook/flows/planner.py +++ b/passbook/flows/planner.py @@ -9,7 +9,8 @@ from structlog import get_logger from passbook.core.models import User from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException -from passbook.flows.models import Flow, Stage +from passbook.flows.markers import ReevaluateMarker, StageMarker +from passbook.flows.models import Flow, FlowStageBinding, Stage from passbook.policies.engine import PolicyEngine LOGGER = get_logger() @@ -33,12 +34,39 @@ class FlowPlan: of all Stages that should be run.""" flow_pk: str + stages: List[Stage] = field(default_factory=list) context: Dict[str, Any] = field(default_factory=dict) + markers: List[StageMarker] = field(default_factory=list) - def next(self) -> Stage: + def next(self) -> Optional[Stage]: """Return next pending stage from the bottom of the list""" - return self.stages[0] + if not self.has_stages: + return None + stage = self.stages[0] + marker = self.markers[0] + + LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker) + marked_stage = marker.process(self, stage) + if not marked_stage: + LOGGER.debug("f(plan_inst): marker returned none, next stage", stage=stage) + self.stages.remove(stage) + self.markers.remove(marker) + if not self.has_stages: + return None + # pylint: disable=not-callable + return self.next() + return marked_stage + + def pop(self): + """Pop next pending stage from bottom of list""" + self.markers.pop(0) + self.stages.pop(0) + + @property + def has_stages(self) -> bool: + """Check if there are any stages left in this plan""" + return len(self.markers) + len(self.stages) > 0 class FlowPlanner: @@ -100,7 +128,8 @@ class FlowPlanner: request: HttpRequest, default_context: Optional[Dict[str, Any]], ) -> FlowPlan: - """Actually build flow plan""" + """Build flow plan by checking each stage in their respective + order and checking the applied policies""" start_time = time() plan = FlowPlan(flow_pk=self.flow.pk.hex) if default_context: @@ -111,13 +140,24 @@ class FlowPlanner: .select_subclasses() .select_related() ): - binding = stage.flowstagebinding_set.get(flow__pk=self.flow.pk) + binding: FlowStageBinding = stage.flowstagebinding_set.get( + flow__pk=self.flow.pk + ) engine = PolicyEngine(binding, user, request) engine.request.context = plan.context engine.build() if engine.passing: LOGGER.debug("f(plan): Stage passing", stage=stage, flow=self.flow) plan.stages.append(stage) + marker = StageMarker() + if binding.re_evaluate_policies: + LOGGER.debug( + "f(plan): Stage has re-evaluate marker", + stage=stage, + flow=self.flow, + ) + marker = ReevaluateMarker(binding=binding, user=user) + plan.markers.append(marker) end_time = time() LOGGER.debug( "f(plan): Finished building", diff --git a/passbook/flows/templates/flows/shell.html b/passbook/flows/templates/flows/shell.html index 08190994d..8957901b5 100644 --- a/passbook/flows/templates/flows/shell.html +++ b/passbook/flows/templates/flows/shell.html @@ -57,7 +57,7 @@

@@ -120,6 +120,7 @@ const updateCard = (data) => { break; case "template": flowBody.innerHTML = data.body; + checkAutofocus(); updateMessages(); loadFormCode(); setFormSubmitHandlers(); @@ -138,6 +139,12 @@ const loadFormCode = () => { document.head.appendChild(newScript); }); }; +const checkAutofocus = () => { + const autofocusElement = document.querySelector("#flow-body [autofocus]"); + if (autofocusElement !== null) { + autofocusElement.focus(); + } +}; const updateFormAction = (form) => { for (let index = 0; index < form.elements.length; index++) { const element = form.elements[index]; diff --git a/passbook/flows/tests/test_planner.py b/passbook/flows/tests/test_planner.py index af6ae98fa..8c65e6636 100644 --- a/passbook/flows/tests/test_planner.py +++ b/passbook/flows/tests/test_planner.py @@ -1,6 +1,7 @@ """flow planner tests""" from unittest.mock import MagicMock, PropertyMock, patch +from django.contrib.sessions.middleware import SessionMiddleware from django.core.cache import cache from django.shortcuts import reverse from django.test import RequestFactory, TestCase @@ -8,14 +9,19 @@ from guardian.shortcuts import get_anonymous_user from passbook.core.models import User from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException +from passbook.flows.markers import ReevaluateMarker, StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key +from passbook.policies.dummy.models import DummyPolicy +from passbook.policies.models import PolicyBinding from passbook.policies.types import PolicyResult from passbook.stages.dummy.models import DummyStage -POLICY_RESULT_MOCK = PropertyMock(return_value=PolicyResult(False)) +POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False)) TIME_NOW_MOCK = MagicMock(return_value=3) +POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True)) + class TestFlowPlanner(TestCase): """Test planner logic""" @@ -40,7 +46,7 @@ class TestFlowPlanner(TestCase): planner.plan(request) @patch( - "passbook.policies.engine.PolicyEngine.result", POLICY_RESULT_MOCK, + "passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE, ) def test_non_applicable_plan(self): """Test that empty plan raises exception""" @@ -103,3 +109,71 @@ class TestFlowPlanner(TestCase): planner.plan(request, default_context={PLAN_CONTEXT_PENDING_USER: user}) key = cache_key(flow, user) self.assertTrue(cache.get(key) is not None) + + def test_planner_marker_reevaluate(self): + """Test that the planner creates the proper marker""" + flow = Flow.objects.create( + name="test-default-context", + slug="test-default-context", + designation=FlowDesignation.AUTHENTICATION, + ) + + FlowStageBinding.objects.create( + flow=flow, + stage=DummyStage.objects.create(name="dummy1"), + order=0, + re_evaluate_policies=True, + ) + + request = self.request_factory.get( + reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + ) + request.user = get_anonymous_user() + + planner = FlowPlanner(flow) + plan = planner.plan(request) + + self.assertIsInstance(plan.markers[0], ReevaluateMarker) + + def test_planner_reevaluate_actual(self): + """Test planner with re-evaluate""" + flow = Flow.objects.create( + name="test-default-context", + slug="test-default-context", + designation=FlowDesignation.AUTHENTICATION, + ) + false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2) + + binding = FlowStageBinding.objects.create( + flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0 + ) + binding2 = FlowStageBinding.objects.create( + flow=flow, + stage=DummyStage.objects.create(name="dummy2"), + order=1, + re_evaluate_policies=True, + ) + + PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) + + request = self.request_factory.get( + reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + ) + request.user = get_anonymous_user() + + middleware = SessionMiddleware() + middleware.process_request(request) + request.session.save() + + # Here we patch the dummy policy to evaluate to true so the stage is included + with patch( + "passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE + ): + planner = FlowPlanner(flow) + plan = planner.plan(request) + + self.assertEqual(plan.stages[0], binding.stage) + self.assertEqual(plan.stages[1], binding2.stage) + + self.assertIsInstance(plan.markers[0], StageMarker) + self.assertIsInstance(plan.markers[1], ReevaluateMarker) diff --git a/passbook/flows/tests/test_views.py b/passbook/flows/tests/test_views.py index cacbe2004..f68173582 100644 --- a/passbook/flows/tests/test_views.py +++ b/passbook/flows/tests/test_views.py @@ -3,16 +3,21 @@ from unittest.mock import MagicMock, PropertyMock, patch from django.shortcuts import reverse from django.test import Client, TestCase +from django.utils.encoding import force_text from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException +from passbook.flows.markers import ReevaluateMarker, StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import FlowPlan from passbook.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN from passbook.lib.config import CONFIG +from passbook.policies.dummy.models import DummyPolicy +from passbook.policies.models import PolicyBinding from passbook.policies.types import PolicyResult from passbook.stages.dummy.models import DummyStage -POLICY_RESULT_MOCK = PropertyMock(return_value=PolicyResult(False)) +POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False)) +POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True)) class TestFlowExecutor(TestCase): @@ -29,7 +34,9 @@ class TestFlowExecutor(TestCase): designation=FlowDesignation.AUTHENTICATION, ) stage = DummyStage.objects.create(name="dummy") - plan = FlowPlan(flow_pk=flow.pk.hex + "a", stages=[stage]) + plan = FlowPlan( + flow_pk=flow.pk.hex + "a", stages=[stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -45,7 +52,7 @@ class TestFlowExecutor(TestCase): self.assertEqual(cancel_mock.call_count, 1) @patch( - "passbook.policies.engine.PolicyEngine.result", POLICY_RESULT_MOCK, + "passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE, ) def test_invalid_non_applicable_flow(self): """Tests that a non-applicable flow returns the correct error message""" @@ -125,3 +132,197 @@ class TestFlowExecutor(TestCase): session = self.client.session plan: FlowPlan = session[SESSION_KEY_PLAN] self.assertEqual(len(plan.stages), 1) + + def test_reevaluate_remove_last(self): + """Test planner with re-evaluate (last stage is removed)""" + flow = Flow.objects.create( + name="test-default-context", + slug="test-default-context", + designation=FlowDesignation.AUTHENTICATION, + ) + false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2) + + binding = FlowStageBinding.objects.create( + flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0 + ) + binding2 = FlowStageBinding.objects.create( + flow=flow, + stage=DummyStage.objects.create(name="dummy2"), + order=1, + re_evaluate_policies=True, + ) + + PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) + + # Here we patch the dummy policy to evaluate to true so the stage is included + with patch( + "passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE + ): + + exec_url = reverse( + "passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug} + ) + # First request, run the planner + response = self.client.get(exec_url) + self.assertEqual(response.status_code, 200) + + plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] + + self.assertEqual(plan.stages[0], binding.stage) + self.assertEqual(plan.stages[1], binding2.stage) + + self.assertIsInstance(plan.markers[0], StageMarker) + self.assertIsInstance(plan.markers[1], ReevaluateMarker) + + # Second request, this passes the first dummy stage + response = self.client.post(exec_url) + self.assertEqual(response.status_code, 302) + + # third request, this should trigger the re-evaluate + # We do this request without the patch, so the policy results in false + response = self.client.post(exec_url) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, reverse("passbook_core:overview")) + + def test_reevaluate_remove_middle(self): + """Test planner with re-evaluate (middle stage is removed)""" + flow = Flow.objects.create( + name="test-default-context", + slug="test-default-context", + designation=FlowDesignation.AUTHENTICATION, + ) + false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2) + + binding = FlowStageBinding.objects.create( + flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0 + ) + binding2 = FlowStageBinding.objects.create( + flow=flow, + stage=DummyStage.objects.create(name="dummy2"), + order=1, + re_evaluate_policies=True, + ) + binding3 = FlowStageBinding.objects.create( + flow=flow, stage=DummyStage.objects.create(name="dummy3"), order=2 + ) + + PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) + + # Here we patch the dummy policy to evaluate to true so the stage is included + with patch( + "passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE + ): + + exec_url = reverse( + "passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug} + ) + # First request, run the planner + response = self.client.get(exec_url) + + self.assertEqual(response.status_code, 200) + plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] + + self.assertEqual(plan.stages[0], binding.stage) + self.assertEqual(plan.stages[1], binding2.stage) + self.assertEqual(plan.stages[2], binding3.stage) + + self.assertIsInstance(plan.markers[0], StageMarker) + self.assertIsInstance(plan.markers[1], ReevaluateMarker) + self.assertIsInstance(plan.markers[2], StageMarker) + + # Second request, this passes the first dummy stage + response = self.client.post(exec_url) + self.assertEqual(response.status_code, 302) + + plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] + + self.assertEqual(plan.stages[0], binding2.stage) + self.assertEqual(plan.stages[1], binding3.stage) + + self.assertIsInstance(plan.markers[0], StageMarker) + self.assertIsInstance(plan.markers[1], StageMarker) + + # third request, this should trigger the re-evaluate + # We do this request without the patch, so the policy results in false + response = self.client.post(exec_url) + self.assertEqual(response.status_code, 200) + self.assertJSONEqual( + force_text(response.content), + {"type": "redirect", "to": reverse("passbook_core:overview")}, + ) + + def test_reevaluate_remove_consecutive(self): + """Test planner with re-evaluate (consecutive stages are removed)""" + flow = Flow.objects.create( + name="test-default-context", + slug="test-default-context", + designation=FlowDesignation.AUTHENTICATION, + ) + false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2) + + binding = FlowStageBinding.objects.create( + flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0 + ) + binding2 = FlowStageBinding.objects.create( + flow=flow, + stage=DummyStage.objects.create(name="dummy2"), + order=1, + re_evaluate_policies=True, + ) + binding3 = FlowStageBinding.objects.create( + flow=flow, + stage=DummyStage.objects.create(name="dummy3"), + order=2, + re_evaluate_policies=True, + ) + binding4 = FlowStageBinding.objects.create( + flow=flow, stage=DummyStage.objects.create(name="dummy4"), order=2 + ) + + PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) + PolicyBinding.objects.create(policy=false_policy, target=binding3, order=0) + + # Here we patch the dummy policy to evaluate to true so the stage is included + with patch( + "passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE + ): + + exec_url = reverse( + "passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug} + ) + # First request, run the planner + response = self.client.get(exec_url) + self.assertEqual(response.status_code, 200) + self.assertIn("dummy1", force_text(response.content)) + + plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] + + self.assertEqual(plan.stages[0], binding.stage) + self.assertEqual(plan.stages[1], binding2.stage) + self.assertEqual(plan.stages[2], binding3.stage) + self.assertEqual(plan.stages[3], binding4.stage) + + self.assertIsInstance(plan.markers[0], StageMarker) + self.assertIsInstance(plan.markers[1], ReevaluateMarker) + self.assertIsInstance(plan.markers[2], ReevaluateMarker) + self.assertIsInstance(plan.markers[3], StageMarker) + + # Second request, this passes the first dummy stage + response = self.client.post(exec_url) + self.assertEqual(response.status_code, 302) + + # third request, this should trigger the re-evaluate + # A get request will evaluate the policies and this will return stage 4 + # but it won't save it, hence we cant' check the plan + response = self.client.get(exec_url) + self.assertEqual(response.status_code, 200) + self.assertIn("dummy4", force_text(response.content)) + + # fourth request, this confirms the last stage (dummy4) + # We do this request without the patch, so the policy results in false + response = self.client.post(exec_url) + self.assertEqual(response.status_code, 200) + self.assertJSONEqual( + force_text(response.content), + {"type": "redirect", "to": reverse("passbook_core:overview")}, + ) diff --git a/passbook/flows/tests/test_views_helper.py b/passbook/flows/tests/test_views_helper.py index 0f59e4b7e..435146f5b 100644 --- a/passbook/flows/tests/test_views_helper.py +++ b/passbook/flows/tests/test_views_helper.py @@ -26,7 +26,7 @@ class TestHelperView(TestCase): def test_default_view_invalid_plan(self): """Test that ToDefaultFlow returns the expected URL (with an invalid plan)""" flow = Flow.objects.filter(designation=FlowDesignation.INVALIDATION,).first() - plan = FlowPlan(flow_pk=flow.pk.hex + "aa", stages=[]) + plan = FlowPlan(flow_pk=flow.pk.hex + "aa") session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() diff --git a/passbook/flows/views.py b/passbook/flows/views.py index 3613a7017..affbe4536 100644 --- a/passbook/flows/views.py +++ b/passbook/flows/views.py @@ -86,6 +86,9 @@ class FlowExecutorView(View): current_stage=self.current_stage, flow_slug=self.flow.slug, ) + if not self.current_stage: + LOGGER.debug("f(exec): no more stages, flow is done.") + return self._flow_done() stage_cls = path_to_class(self.current_stage.type) self.current_stage_view = stage_cls(self) self.current_stage_view.args = self.args @@ -98,6 +101,7 @@ class FlowExecutorView(View): LOGGER.debug( "f(exec): Passing GET", view_class=class_to_path(self.current_stage_view.__class__), + stage=self.current_stage, flow_slug=self.flow.slug, ) stage_response = self.current_stage_view.get(request, *args, **kwargs) @@ -108,6 +112,7 @@ class FlowExecutorView(View): LOGGER.debug( "f(exec): Passing POST", view_class=class_to_path(self.current_stage_view.__class__), + stage=self.current_stage, flow_slug=self.flow.slug, ) stage_response = self.current_stage_view.post(request, *args, **kwargs) @@ -133,7 +138,11 @@ class FlowExecutorView(View): stage_class=class_to_path(self.current_stage_view.__class__), flow_slug=self.flow.slug, ) - self.plan.stages.pop(0) + # We call plan.next here to check for re-evaluate markers + # this is important so we can save the result + # and we don't have to re-evaluate the policies each request + self.plan.next() + self.plan.pop() self.request.session[SESSION_KEY_PLAN] = self.plan if self.plan.stages: LOGGER.debug( diff --git a/passbook/policies/expression/templates/policy/expression/form.html b/passbook/policies/expression/templates/policy/expression/form.html index 14bfa626e..95b3a8fb9 100644 --- a/passbook/policies/expression/templates/policy/expression/form.html +++ b/passbook/policies/expression/templates/policy/expression/form.html @@ -7,23 +7,8 @@

- Expression using Jinja. Following variables are available: + Expression using Python. See here for a list of all variables.

-
    -
  • request.user: Passbook User Object (Reference)
  • -
  • request.http_request: Django HTTP Request Object (Reference)
  • -
  • request.obj: Model the Policy is run against.
  • -
  • 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 group_name.
  • -
  • pb_logger: Standard Python Logger Object, which can be used to debug expressions.
  • -
  • pb_client_ip: Client's IP Address.
  • -
-

Custom Filters:

-
    -
  • regex_match(regex): Checks if value matches regex
  • -
  • regex_replace(regex, repl): Replace string matched by regex with repl
  • -
{% endblock %} diff --git a/passbook/providers/app_gw/templates/app_gw/docker-compose.yml b/passbook/providers/app_gw/templates/app_gw/docker-compose.yml index 14b7cbd02..6e65148a6 100644 --- a/passbook/providers/app_gw/templates/app_gw/docker-compose.yml +++ b/passbook/providers/app_gw/templates/app_gw/docker-compose.yml @@ -2,7 +2,6 @@ version: "3.5" services: passbook_gatekeeper: - container_name: gatekeeper image: beryju/passbook-gatekeeper:{{ version }} ports: - 4180:4180 diff --git a/passbook/providers/oauth/settings.py b/passbook/providers/oauth/settings.py index bd04dd43e..925787a30 100644 --- a/passbook/providers/oauth/settings.py +++ b/passbook/providers/oauth/settings.py @@ -23,11 +23,9 @@ OAUTH2_PROVIDER = { # this is the list of available scopes "SCOPES": { "openid": "Access OpenID Userinfo", - "openid:userinfo": "Access OpenID Userinfo", - "email": "Access OpenID E-Mail", - # 'write': 'Write scope', - # 'groups': 'Access to your groups', - "user:email": "GitHub Compatibility: User E-Mail", + "userinfo": "Access OpenID Userinfo", + "email": "Access OpenID Email", + "user:email": "GitHub Compatibility: User Email", "read:org": "GitHub Compatibility: User Groups", } } diff --git a/passbook/providers/saml/forms.py b/passbook/providers/saml/forms.py index 864e36a74..01f44e519 100644 --- a/passbook/providers/saml/forms.py +++ b/passbook/providers/saml/forms.py @@ -2,8 +2,10 @@ from django import forms from django.contrib.admin.widgets import FilteredSelectMultiple +from django.utils.html import mark_safe from django.utils.translation import gettext as _ +from passbook.admin.fields import CodeMirrorWidget from passbook.core.expression import PropertyMappingEvaluator from passbook.flows.models import Flow, FlowDesignation from passbook.providers.saml.models import ( @@ -74,4 +76,13 @@ class SAMLPropertyMappingForm(forms.ModelForm): "name": forms.TextInput(), "saml_name": forms.TextInput(), "friendly_name": forms.TextInput(), + "expression": CodeMirrorWidget(mode="python"), + } + help_texts = { + "saml_name": mark_safe( + _( + "URN OID used by SAML. This is optional. " + 'Reference' + ) + ), } diff --git a/passbook/providers/saml/templates/saml/idp/property_mapping_form.html b/passbook/providers/saml/templates/saml/idp/property_mapping_form.html index 7b5d3f36e..4bd3085af 100644 --- a/passbook/providers/saml/templates/saml/idp/property_mapping_form.html +++ b/passbook/providers/saml/templates/saml/idp/property_mapping_form.html @@ -7,12 +7,7 @@

- Expression using Jinja. Following variables are available: -

    -
  • user: Passbook User Object (Reference)
  • -
  • request: Django HTTP Request Object (Reference)
  • -
  • provider: Passbook SAML Provider Object (Reference)
  • -
+ Expression using Python. See here for a list of all variables.

diff --git a/passbook/providers/saml/views.py b/passbook/providers/saml/views.py index a8497fed9..8c182ecc3 100644 --- a/passbook/providers/saml/views.py +++ b/passbook/providers/saml/views.py @@ -102,7 +102,7 @@ class SAMLSSOBindingRedirectView(SAMLSSOView): """SAML Handler for SSO/Redirect bindings, which are sent via GET""" # pylint: disable=unused-argument - def get( + def get( # lgtm [py/similar-function] self, request: HttpRequest, application_slug: str ) -> Optional[HttpResponse]: """Handle REDIRECT bindings""" diff --git a/passbook/root/settings.py b/passbook/root/settings.py index 8e239929d..e9978e8fa 100644 --- a/passbook/root/settings.py +++ b/passbook/root/settings.py @@ -188,7 +188,7 @@ WSGI_APPLICATION = "passbook.root.wsgi.application" DATABASES = { "default": { - "ENGINE": "django_prometheus.db.backends.postgresql", + "ENGINE": "django.db.backends.postgresql", "HOST": CONFIG.y("postgresql.host"), "NAME": CONFIG.y("postgresql.name"), "USER": CONFIG.y("postgresql.user"), diff --git a/passbook/sources/ldap/forms.py b/passbook/sources/ldap/forms.py index 6536241d8..187a9d5dd 100644 --- a/passbook/sources/ldap/forms.py +++ b/passbook/sources/ldap/forms.py @@ -4,6 +4,7 @@ from django import forms from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext_lazy as _ +from passbook.admin.fields import CodeMirrorWidget from passbook.admin.forms.source import SOURCE_FORM_FIELDS from passbook.core.expression import PropertyMappingEvaluator from passbook.sources.ldap.models import LDAPPropertyMapping, LDAPSource @@ -68,4 +69,8 @@ class LDAPPropertyMappingForm(forms.ModelForm): "name": forms.TextInput(), "ldap_property": forms.TextInput(), "object_field": forms.TextInput(), + "expression": CodeMirrorWidget(mode="python"), + } + help_texts = { + "object_field": _("Field of the user object this value is written to.") } diff --git a/passbook/sources/ldap/models.py b/passbook/sources/ldap/models.py index 34fa96e56..4759463fb 100644 --- a/passbook/sources/ldap/models.py +++ b/passbook/sources/ldap/models.py @@ -55,7 +55,7 @@ class LDAPSource(Source): form = "passbook.sources.ldap.forms.LDAPSourceForm" - _connection: Optional[Connection] + _connection: Optional[Connection] = None @property def connection(self) -> Connection: diff --git a/passbook/sources/ldap/templates/ldap/property_mapping_form.html b/passbook/sources/ldap/templates/ldap/property_mapping_form.html index 63887ba7d..4bd3085af 100644 --- a/passbook/sources/ldap/templates/ldap/property_mapping_form.html +++ b/passbook/sources/ldap/templates/ldap/property_mapping_form.html @@ -7,10 +7,7 @@

- Expression using Jinja. Following variables are available: -

    -
  • ldap: A Dictionary of all values retrieved from LDAP.
  • -
+ Expression using Python. See here for a list of all variables.

diff --git a/passbook/sources/oauth/clients.py b/passbook/sources/oauth/clients.py index 35c58d7ba..0b3cb85f8 100644 --- a/passbook/sources/oauth/clients.py +++ b/passbook/sources/oauth/clients.py @@ -94,8 +94,6 @@ class OAuthClient(BaseOAuthClient): "oauth_callback": callback, "token": raw_token, } - callback = request.build_absolute_uri(callback or request.path) - callback = force_text(callback) try: response = self.session.request( "post", diff --git a/passbook/stages/captcha/tests.py b/passbook/stages/captcha/tests.py index f5818deb3..c5c8f5850 100644 --- a/passbook/stages/captcha/tests.py +++ b/passbook/stages/captcha/tests.py @@ -5,6 +5,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import FlowPlan from passbook.flows.views import SESSION_KEY_PLAN @@ -35,7 +36,9 @@ class TestCaptchaStage(TestCase): def test_valid(self): """Test valid captcha""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() diff --git a/passbook/stages/consent/tests.py b/passbook/stages/consent/tests.py index 4d642d495..e7f0f2534 100644 --- a/passbook/stages/consent/tests.py +++ b/passbook/stages/consent/tests.py @@ -4,6 +4,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import FlowPlan from passbook.flows.views import SESSION_KEY_PLAN @@ -30,7 +31,9 @@ class TestConsentStage(TestCase): def test_valid(self): """Test valid consent""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() diff --git a/passbook/stages/dummy/stage.py b/passbook/stages/dummy/stage.py index 43a3edcdf..07f3dacff 100644 --- a/passbook/stages/dummy/stage.py +++ b/passbook/stages/dummy/stage.py @@ -1,4 +1,6 @@ """passbook multi-stage authentication engine""" +from typing import Any, Dict + from django.http import HttpRequest from passbook.flows.stage import StageView @@ -10,3 +12,8 @@ class DummyStage(StageView): def post(self, request: HttpRequest): """Just redirect to next stage""" return self.executor.stage_ok() + + def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]: + kwargs = super().get_context_data(**kwargs) + kwargs["title"] = self.executor.current_stage.name + return kwargs diff --git a/passbook/stages/email/forms.py b/passbook/stages/email/forms.py index e158743e1..03db70f4b 100644 --- a/passbook/stages/email/forms.py +++ b/passbook/stages/email/forms.py @@ -6,13 +6,13 @@ from passbook.stages.email.models import EmailStage class EmailStageSendForm(forms.Form): - """Form used when sending the e-mail to prevent multiple emails being sent""" + """Form used when sending the email to prevent multiple emails being sent""" invalid = forms.CharField(widget=forms.HiddenInput, required=True) class EmailStageForm(forms.ModelForm): - """Form to create/edit E-Mail Stage""" + """Form to create/edit Email Stage""" class Meta: @@ -34,6 +34,7 @@ class EmailStageForm(forms.ModelForm): widgets = { "name": forms.TextInput(), "host": forms.TextInput(), + "subject": forms.TextInput(), "username": forms.TextInput(), "password": forms.TextInput(), } diff --git a/passbook/stages/email/models.py b/passbook/stages/email/models.py index 4eef283df..bcdec174e 100644 --- a/passbook/stages/email/models.py +++ b/passbook/stages/email/models.py @@ -8,7 +8,7 @@ from passbook.flows.models import Stage class EmailTemplates(models.TextChoices): - """Templates used for rendering the E-Mail""" + """Templates used for rendering the Email""" PASSWORD_RESET = ( "stages/email/for_email/password_reset.html", diff --git a/passbook/stages/email/stage.py b/passbook/stages/email/stage.py index e0dce139b..a93ab563e 100644 --- a/passbook/stages/email/stage.py +++ b/passbook/stages/email/stage.py @@ -22,7 +22,7 @@ QS_KEY_TOKEN = "token" class EmailStageView(FormView, StageView): - """E-Mail stage which sends E-Mail for verification""" + """Email stage which sends Email for verification""" form_class = EmailStageSendForm template_name = "stages/email/waiting_message.html" @@ -41,11 +41,14 @@ class EmailStageView(FormView, StageView): token = get_object_or_404(Token, pk=request.GET[QS_KEY_TOKEN]) self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = token.user token.delete() - messages.success(request, _("Successfully verified E-Mail.")) + messages.success(request, _("Successfully verified Email.")) return self.executor.stage_ok() return super().get(request, *args, **kwargs) def form_invalid(self, form: EmailStageSendForm) -> HttpResponse: + if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context: + messages.error(self.request, _("No pending user.")) + return super().form_invalid(form) pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] valid_delta = timedelta( minutes=self.executor.current_stage.token_expiry + 1 diff --git a/passbook/stages/email/tasks.py b/passbook/stages/email/tasks.py index 750a5326e..846df7c55 100644 --- a/passbook/stages/email/tasks.py +++ b/passbook/stages/email/tasks.py @@ -27,7 +27,7 @@ def send_mails(stage: EmailStage, *messages: List[EmailMultiAlternatives]): ) # pylint: disable=unused-argument def _send_mail_task(self, email_stage_pk: int, message: Dict[Any, Any]): - """Send E-Mail according to EmailStage parameters from background worker. + """Send Email according to EmailStage parameters from background worker. Automatically retries if message couldn't be sent.""" stage: EmailStage = EmailStage.objects.get(pk=email_stage_pk) backend = stage.backend diff --git a/passbook/stages/email/templates/stages/email/for_email/account_confirm.html b/passbook/stages/email/templates/stages/email/for_email/account_confirm.html index f300b6585..4c38ccf38 100644 --- a/passbook/stages/email/templates/stages/email/for_email/account_confirm.html +++ b/passbook/stages/email/templates/stages/email/for_email/account_confirm.html @@ -18,7 +18,7 @@
- +
{% trans 'Confirm Account' %} {% trans 'Confirm Account' %}
diff --git a/passbook/stages/email/templates/stages/email/for_email/password_reset.html b/passbook/stages/email/templates/stages/email/for_email/password_reset.html index d43467958..4003b1e9c 100644 --- a/passbook/stages/email/templates/stages/email/for_email/password_reset.html +++ b/passbook/stages/email/templates/stages/email/for_email/password_reset.html @@ -23,7 +23,7 @@ - +
{% trans 'Reset Password' %} {% trans 'Reset Password' %}
@@ -33,7 +33,7 @@

{% blocktrans with expires=expires|naturaltime %} - If you did not request a password change, please ignore this E-Mail. The link above is valid for {{ expires }}. + If you did not request a password change, please ignore this Email. The link above is valid for {{ expires }}. {% endblocktrans %}

diff --git a/passbook/stages/email/templates/stages/email/waiting_message.html b/passbook/stages/email/templates/stages/email/waiting_message.html index bebfb7317..a1c0a2a64 100644 --- a/passbook/stages/email/templates/stages/email/waiting_message.html +++ b/passbook/stages/email/templates/stages/email/waiting_message.html @@ -7,7 +7,7 @@

{% blocktrans %} - Check your E-Mails for a password reset link. + Check your Emails for a password reset link. {% endblocktrans %}

{% csrf_token %} @@ -15,7 +15,7 @@ {% block beneath_form %} {% endblock %}
- +
{% endblock %} diff --git a/passbook/stages/email/tests.py b/passbook/stages/email/tests.py index c65ebff09..43d7e2e7c 100644 --- a/passbook/stages/email/tests.py +++ b/passbook/stages/email/tests.py @@ -7,6 +7,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import Token, User +from passbook.flows.markers import StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from passbook.flows.views import SESSION_KEY_PLAN @@ -34,7 +35,9 @@ class TestEmailStage(TestCase): def test_rendering(self): """Test with pending user""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan @@ -48,7 +51,9 @@ class TestEmailStage(TestCase): def test_without_user(self): """Test without pending user""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -61,7 +66,9 @@ class TestEmailStage(TestCase): def test_pending_user(self): """Test with pending user""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan @@ -82,7 +89,9 @@ class TestEmailStage(TestCase): """Test with token""" # Make sure token exists self.test_pending_user() - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() diff --git a/passbook/stages/identification/forms.py b/passbook/stages/identification/forms.py index 882ce0f03..e99292640 100644 --- a/passbook/stages/identification/forms.py +++ b/passbook/stages/identification/forms.py @@ -1,5 +1,6 @@ """passbook flows identification forms""" from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple from django.core.validators import validate_email from django.utils.translation import gettext_lazy as _ from structlog import get_logger @@ -19,6 +20,9 @@ class IdentificationStageForm(forms.ModelForm): fields = ["name", "user_fields", "template", "enrollment_flow", "recovery_flow"] widgets = { "name": forms.TextInput(), + "user_fields": FilteredSelectMultiple( + _("fields"), False, choices=UserFields.choices + ), } @@ -35,8 +39,16 @@ class IdentificationForm(forms.Form): super().__init__(*args, **kwargs) if self.stage.user_fields == [UserFields.E_MAIL]: self.fields["uid_field"] = forms.EmailField() - self.fields["uid_field"].label = human_list( - [x.title() for x in self.stage.user_fields] + label = human_list([x.title() for x in self.stage.user_fields]) + self.fields["uid_field"].label = label + self.fields["uid_field"].widget.attrs.update( + { + "placeholder": _(label), + "autofocus": "autofocus", + # Autocomplete according to + # https://www.chromium.org/developers/design-documents/form-styles-that-chromium-understands + "autocomplete": "username", + } ) def clean_uid_field(self): diff --git a/passbook/stages/identification/migrations/0003_auto_20200615_1641.py b/passbook/stages/identification/migrations/0003_auto_20200615_1641.py new file mode 100644 index 000000000..a6451a360 --- /dev/null +++ b/passbook/stages/identification/migrations/0003_auto_20200615_1641.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.7 on 2020-06-15 16:41 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_flows", "0005_provider_flows"), + ("passbook_stages_identification", "0002_auto_20200530_2204"), + ] + + operations = [ + migrations.AlterField( + model_name="identificationstage", + name="recovery_flow", + field=models.ForeignKey( + blank=True, + default=None, + help_text="Optional recovery flow, which is linked at the bottom of the page.", + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + related_name="+", + to="passbook_flows.Flow", + ), + ), + ] diff --git a/passbook/stages/identification/models.py b/passbook/stages/identification/models.py index 94bf0ec48..f225b40e4 100644 --- a/passbook/stages/identification/models.py +++ b/passbook/stages/identification/models.py @@ -48,7 +48,7 @@ class IdentificationStage(Stage): related_name="+", default=None, help_text=_( - "Optional enrollment flow, which is linked at the bottom of the page." + "Optional recovery flow, which is linked at the bottom of the page." ), ) diff --git a/passbook/stages/identification/tests.py b/passbook/stages/identification/tests.py index f2d2835a8..37eb67c51 100644 --- a/passbook/stages/identification/tests.py +++ b/passbook/stages/identification/tests.py @@ -61,7 +61,7 @@ class TestIdentificationStage(TestCase): ) def test_invalid_with_username(self): - """Test invalid with username (user exists but stage only allows e-mail)""" + """Test invalid with username (user exists but stage only allows email)""" form_data = {"uid_field": self.user.username} response = self.client.post( reverse( @@ -72,7 +72,7 @@ class TestIdentificationStage(TestCase): self.assertEqual(response.status_code, 200) def test_invalid_with_invalid_email(self): - """Test with invalid e-mail (user doesn't exist) -> Will return to login form""" + """Test with invalid email (user doesn't exist) -> Will return to login form""" form_data = {"uid_field": self.user.email + "test"} response = self.client.post( reverse( diff --git a/passbook/stages/invitation/stage.py b/passbook/stages/invitation/stage.py index 2ec3a5de7..be1582775 100644 --- a/passbook/stages/invitation/stage.py +++ b/passbook/stages/invitation/stage.py @@ -7,6 +7,7 @@ from passbook.stages.invitation.models import Invitation, InvitationStage from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT INVITATION_TOKEN_KEY = "token" +INVITATION_IN_EFFECT = "invitation_in_effect" class InvitationStageView(StageView): @@ -23,4 +24,5 @@ class InvitationStageView(StageView): token = request.GET[INVITATION_TOKEN_KEY] invite: Invitation = get_object_or_404(Invitation, pk=token) self.executor.plan.context[PLAN_CONTEXT_PROMPT] = invite.fixed_data + self.executor.plan.context[INVITATION_IN_EFFECT] = True return self.executor.stage_ok() diff --git a/passbook/stages/invitation/tests.py b/passbook/stages/invitation/tests.py index 4e7dbb7db..f47ddc766 100644 --- a/passbook/stages/invitation/tests.py +++ b/passbook/stages/invitation/tests.py @@ -7,6 +7,7 @@ from django.utils.encoding import force_text from guardian.shortcuts import get_anonymous_user from passbook.core.models import User +from passbook.flows.markers import StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from passbook.flows.views import SESSION_KEY_PLAN @@ -39,7 +40,9 @@ class TestUserLoginStage(TestCase): def test_without_invitation_fail(self): """Test without any invitation, continue_flow_without_invitation not set.""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[ PLAN_CONTEXT_AUTHENTICATION_BACKEND @@ -64,7 +67,9 @@ class TestUserLoginStage(TestCase): """Test without any invitation, continue_flow_without_invitation is set.""" self.stage.continue_flow_without_invitation = True self.stage.save() - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[ PLAN_CONTEXT_AUTHENTICATION_BACKEND @@ -90,7 +95,9 @@ class TestUserLoginStage(TestCase): def test_with_invitation(self): """Test with invitation, check data in session""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[ PLAN_CONTEXT_AUTHENTICATION_BACKEND diff --git a/passbook/stages/otp/views.py b/passbook/stages/otp/views.py index e933bedec..82ff8b333 100644 --- a/passbook/stages/otp/views.py +++ b/passbook/stages/otp/views.py @@ -110,7 +110,7 @@ class EnableView(LoginRequiredMixin, FormView): self.static_device = StaticDevice(user=request.user, confirmed=False) self.static_device.save() # Create 9 tokens and save them - # TODO: Send static tokens via E-Mail + # TODO: Send static tokens via Email for _counter in range(0, 9): token = StaticToken( device=self.static_device, token=StaticToken.random_token() diff --git a/passbook/stages/password/forms.py b/passbook/stages/password/forms.py index f9dc91421..41d2bbe7b 100644 --- a/passbook/stages/password/forms.py +++ b/passbook/stages/password/forms.py @@ -1,25 +1,31 @@ """passbook administration forms""" from django import forms -from django.conf import settings from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext_lazy as _ -from passbook.lib.utils.reflection import path_to_class from passbook.stages.password.models import PasswordStage def get_authentication_backends(): """Return all available authentication backends as tuple set""" - for backend in settings.AUTHENTICATION_BACKENDS: - klass = path_to_class(backend) - yield backend, getattr( - klass(), "name", "%s (%s)" % (klass.__name__, klass.__module__) - ) + return [ + ( + "django.contrib.auth.backends.ModelBackend", + _("passbook-internal Userdatabase"), + ), + ( + "passbook.sources.ldap.auth.LDAPBackend", + _("passbook LDAP (Only needed when User-Sync is not enabled."), + ), + ] class PasswordForm(forms.Form): """Password authentication form""" + username = forms.CharField( + widget=forms.HiddenInput(attrs={"autocomplete": "username"}), required=False + ) password = forms.CharField( widget=forms.PasswordInput( attrs={ diff --git a/passbook/stages/password/stage.py b/passbook/stages/password/stage.py index 41abac5f8..8dd708f25 100644 --- a/passbook/stages/password/stage.py +++ b/passbook/stages/password/stage.py @@ -52,9 +52,20 @@ class PasswordStage(FormView, StageView): form_class = PasswordForm template_name = "stages/password/backend.html" + def get_form(self, form_class=None) -> PasswordForm: + form = super().get_form(form_class=form_class) + + # If there's a pending user, update the `username` field + # this field is only used by password managers. + # If there's no user set, an error is raised later. + if PLAN_CONTEXT_PENDING_USER in self.executor.plan.context: + pending_user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] + form.fields["username"].initial = pending_user.username + + return form + def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) - kwargs["primary_action"] = _("Log in") recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY) if recovery_flow.exists(): kwargs["recovery_flow"] = recovery_flow.first() diff --git a/passbook/stages/password/tests.py b/passbook/stages/password/tests.py index 71364abc1..3dbb6f9ec 100644 --- a/passbook/stages/password/tests.py +++ b/passbook/stages/password/tests.py @@ -9,6 +9,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from passbook.flows.views import SESSION_KEY_PLAN @@ -43,7 +44,9 @@ class TestPasswordStage(TestCase): def test_without_user(self): """Test without user""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -68,7 +71,9 @@ class TestPasswordStage(TestCase): designation=FlowDesignation.RECOVERY, slug="qewrqerqr" ) - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -83,7 +88,9 @@ class TestPasswordStage(TestCase): def test_valid_password(self): """Test with a valid pending user and valid password""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan @@ -105,7 +112,9 @@ class TestPasswordStage(TestCase): def test_invalid_password(self): """Test with a valid pending user and invalid password""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan @@ -127,7 +136,9 @@ class TestPasswordStage(TestCase): def test_permission_denied(self): """Test with a valid pending user and valid password. Backend is patched to return PermissionError""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan diff --git a/passbook/stages/prompt/migrations/0003_auto_20200615_1641.py b/passbook/stages/prompt/migrations/0003_auto_20200615_1641.py new file mode 100644 index 000000000..6ba175cc7 --- /dev/null +++ b/passbook/stages/prompt/migrations/0003_auto_20200615_1641.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.7 on 2020-06-15 16:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_stages_prompt", "0002_auto_20200528_2059"), + ] + + operations = [ + migrations.AlterField( + model_name="prompt", + name="type", + field=models.CharField( + choices=[ + ("text", "Text"), + ("username", "Username"), + ("e-mail", "Email"), + ("password", "Password"), + ("number", "Number"), + ("checkbox", "Checkbox"), + ("data", "Date"), + ("data-time", "Date Time"), + ("separator", "Separator"), + ("hidden", "Hidden"), + ("static", "Static"), + ], + max_length=100, + ), + ), + ] diff --git a/passbook/stages/prompt/migrations/0004_auto_20200618_1735.py b/passbook/stages/prompt/migrations/0004_auto_20200618_1735.py new file mode 100644 index 000000000..6e12574a0 --- /dev/null +++ b/passbook/stages/prompt/migrations/0004_auto_20200618_1735.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.7 on 2020-06-18 17:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_stages_prompt", "0003_auto_20200615_1641"), + ] + + operations = [ + migrations.AlterField( + model_name="prompt", + name="type", + field=models.CharField( + choices=[ + ("text", "Text"), + ("username", "Username"), + ("email", "Email"), + ("password", "Password"), + ("number", "Number"), + ("checkbox", "Checkbox"), + ("data", "Date"), + ("data-time", "Date Time"), + ("separator", "Separator"), + ("hidden", "Hidden"), + ("static", "Static"), + ], + max_length=100, + ), + ), + ] diff --git a/passbook/stages/prompt/models.py b/passbook/stages/prompt/models.py index fc12c7b2e..fbdf8835e 100644 --- a/passbook/stages/prompt/models.py +++ b/passbook/stages/prompt/models.py @@ -12,8 +12,11 @@ from passbook.policies.models import PolicyBindingModel class FieldTypes(models.TextChoices): """Field types an Prompt can be""" + # Simple text field TEXT = "text" - EMAIL = "e-mail" + # Same as text, but has autocomplete for password managers + USERNAME = "username" + EMAIL = "email" PASSWORD = "password" # noqa # nosec NUMBER = "number" CHECKBOX = "checkbox" @@ -52,8 +55,11 @@ class Prompt(models.Model): } if self.type == FieldTypes.EMAIL: field_class = forms.EmailField + if self.type == FieldTypes.USERNAME: + attrs["autocomplete"] = "username" if self.type == FieldTypes.PASSWORD: widget = forms.PasswordInput(attrs=attrs) + attrs["autocomplete"] = "new-password" if self.type == FieldTypes.NUMBER: field_class = forms.IntegerField widget = forms.NumberInput(attrs=attrs) @@ -64,6 +70,10 @@ class Prompt(models.Model): if self.type == FieldTypes.CHECKBOX: field_class = forms.CheckboxInput kwargs["required"] = False + if self.type == FieldTypes.DATE: + field_class = forms.DateInput + if self.type == FieldTypes.DATE_TIME: + field_class = forms.DateTimeInput # TODO: Implement static # TODO: Implement separator diff --git a/passbook/stages/prompt/tests.py b/passbook/stages/prompt/tests.py index df8ad88a3..b5893e8d7 100644 --- a/passbook/stages/prompt/tests.py +++ b/passbook/stages/prompt/tests.py @@ -6,6 +6,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import FlowPlan from passbook.flows.views import SESSION_KEY_PLAN @@ -96,7 +97,9 @@ class TestPromptStage(TestCase): def test_render(self): """Test render of form, check if all prompts are rendered correctly""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -114,7 +117,9 @@ class TestPromptStage(TestCase): def test_valid_form_with_policy(self) -> PromptForm: """Test form validation""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) expr = "return request.context['password_prompt'] == request.context['password2_prompt']" expr_policy = ExpressionPolicy.objects.create( name="validate-form", expression=expr @@ -126,7 +131,9 @@ class TestPromptStage(TestCase): def test_invalid_form(self) -> PromptForm: """Test form validation""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) expr = "False" expr_policy = ExpressionPolicy.objects.create( name="validate-form", expression=expr @@ -138,7 +145,9 @@ class TestPromptStage(TestCase): def test_valid_form_request(self): """Test a request with valid form data""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() diff --git a/passbook/stages/user_delete/tests.py b/passbook/stages/user_delete/tests.py index 9d09ddd67..26376c683 100644 --- a/passbook/stages/user_delete/tests.py +++ b/passbook/stages/user_delete/tests.py @@ -4,6 +4,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from passbook.flows.views import SESSION_KEY_PLAN @@ -29,7 +30,9 @@ class TestUserDeleteStage(TestCase): def test_no_user(self): """Test without user set""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -47,7 +50,9 @@ class TestUserDeleteStage(TestCase): def test_user_delete_get(self): """Test Form render""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan @@ -62,7 +67,9 @@ class TestUserDeleteStage(TestCase): def test_user_delete_post(self): """Test User delete (actual)""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan diff --git a/passbook/stages/user_login/tests.py b/passbook/stages/user_login/tests.py index 990550d94..4509c7b50 100644 --- a/passbook/stages/user_login/tests.py +++ b/passbook/stages/user_login/tests.py @@ -4,6 +4,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from passbook.flows.views import SESSION_KEY_PLAN @@ -30,7 +31,9 @@ class TestUserLoginStage(TestCase): def test_valid_password(self): """Test with a valid pending user and backend""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[ PLAN_CONTEXT_AUTHENTICATION_BACKEND @@ -53,7 +56,9 @@ class TestUserLoginStage(TestCase): def test_without_user(self): """Test a plan without any pending user, resulting in a denied""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() @@ -72,7 +77,9 @@ class TestUserLoginStage(TestCase): def test_without_backend(self): """Test a plan with pending user, without backend, resulting in a denied""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan diff --git a/passbook/stages/user_logout/tests.py b/passbook/stages/user_logout/tests.py index ee526a581..7263ccee2 100644 --- a/passbook/stages/user_logout/tests.py +++ b/passbook/stages/user_logout/tests.py @@ -4,6 +4,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from passbook.flows.views import SESSION_KEY_PLAN @@ -30,7 +31,9 @@ class TestUserLogoutStage(TestCase): def test_valid_password(self): """Test with a valid pending user and backend""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[ PLAN_CONTEXT_AUTHENTICATION_BACKEND diff --git a/passbook/stages/user_write/tests.py b/passbook/stages/user_write/tests.py index 8aefbd121..d36ce0c06 100644 --- a/passbook/stages/user_write/tests.py +++ b/passbook/stages/user_write/tests.py @@ -7,6 +7,7 @@ from django.test import Client, TestCase from django.utils.encoding import force_text from passbook.core.models import User +from passbook.flows.markers import StageMarker from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from passbook.flows.views import SESSION_KEY_PLAN @@ -37,7 +38,9 @@ class TestUserWriteStage(TestCase): for _ in range(8) ) - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PROMPT] = { "username": "test-user", "name": "name", @@ -71,7 +74,9 @@ class TestUserWriteStage(TestCase): SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(8) ) - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) plan.context[PLAN_CONTEXT_PENDING_USER] = User.objects.create( username="unittest", email="test@beryju.org" ) @@ -104,7 +109,9 @@ class TestUserWriteStage(TestCase): def test_without_data(self): """Test without data results in error""" - plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan = FlowPlan( + flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] + ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() diff --git a/passbook/static/static/package.json b/passbook/static/static/package.json index 349978a86..17034087a 100644 --- a/passbook/static/static/package.json +++ b/passbook/static/static/package.json @@ -1,8 +1,8 @@ { "license": "MIT", "dependencies": { - "@fortawesome/fontawesome-free": "^5.13.0", - "@patternfly/patternfly": "^2.71.6", + "@fortawesome/fontawesome-free": "^5.13.1", + "@patternfly/patternfly": "^4.10.31", "codemirror": "^5.54.0" } } diff --git a/passbook/static/static/passbook/brand_inverted.svg b/passbook/static/static/passbook/brand_inverted.svg new file mode 100644 index 000000000..86711c6bb --- /dev/null +++ b/passbook/static/static/passbook/brand_inverted.svg @@ -0,0 +1,2 @@ + diff --git a/passbook/static/static/passbook/pf.css b/passbook/static/static/passbook/pf.css index 49ec4eb54..e1de1a829 100644 --- a/passbook/static/static/passbook/pf.css +++ b/passbook/static/static/passbook/pf.css @@ -198,120 +198,6 @@ input[data-is-monospace] { font-family: monospace; } - -.ws-page-header { - background-color: #151515; - min-height: auto -} - -@media (min-width: 992px) { - .ws-page-header .pf-c-page__header-nav { - margin-left:12px - } -} - -.ws-page-header .pf-c-nav__scroll-button { - outline-offset: -4px; - height: 100%; - top: 0 -} - -.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__item { - margin-right: 0 -} - -.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link { - padding-top: 22px; - padding-right: var(--pf-global--spacer--md); - padding-left: var(--pf-global--spacer--md); - color: var(--pf-global--Color--light-100) -} - -@media (max-width: 991px) { - .ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link { - padding-top:10px - } -} - -.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:after { - top: 0!important; - height: 4px -} - -.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:active,.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:hover { - -webkit-transition: .5s; - transition: .5s -} - -.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link.pf-m-current,.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:active,.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:hover { - background-color: var(--pf-global--BackgroundColor--light-100); - color: #151515!important; - font-weight: var(--pf-global--FontWeight--normal) -} - -.ws-page-header li a:after { - content: ""; - position: absolute; - left: 50%!important; - bottom: 0; - -webkit-transform: translateX(-50%) scaleX(0); - transform: translateX(-50%) scaleX(0); - -webkit-transform-origin: 50% 50%; - transform-origin: 50% 50%; - width: 100%; - height: 1px; - background-color: var(--pf-global--BackgroundColor--light-100); - color: #151515!important; - -webkit-transition: -webkit-transform .25s; - transition: -webkit-transform .25s; - transition: transform .25s; - transition: transform .25s,-webkit-transform .25s -} - -.ws-page-header li a:hover:after { - -webkit-transform: translateX(-50%) scaleX(1); - transform: translateX(-50%) scaleX(1) -} - -.ws-page-header li a.pf-m-current:after { - left: 0!important; - -webkit-transform: none; - transform: none -} - -.ws-page-sidebar#page-sidebar { - color: #fff; - box-shadow: none -} - -.ws-page-sidebar .pf-c-nav { - margin-top: 16px -} - -.pf-site-search { - padding: 0 0 2px; - width: 150px; - background: transparent; - -webkit-transition: .25s; - transition: .25s -} - -.ws-page-header .pf-c-page__header-brand-toggle { - display: none; - visibility: hidden -} - -@media (max-width: 768px) { - .pf-site-search { - width:100px - } - - .ws-page-header .pf-c-page__header-brand-toggle { - display: block; - visibility: visible - } -} - /* Form with user */ .form-control-static { display: flex; diff --git a/passbook/static/static/yarn.lock b/passbook/static/static/yarn.lock index 1a9902c0c..fdb9d7547 100644 --- a/passbook/static/static/yarn.lock +++ b/passbook/static/static/yarn.lock @@ -2,15 +2,15 @@ # yarn lockfile v1 -"@fortawesome/fontawesome-free@^5.13.0": - version "5.13.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz#fcb113d1aca4b471b709e8c9c168674fbd6e06d9" - integrity sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg== +"@fortawesome/fontawesome-free@^5.13.1": + version "5.13.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.1.tgz#c53b4066edae16cd1fd669f687baf031b45fb9d6" + integrity sha512-D819f34FLHeBN/4xvw0HR0u7U2G7RqjPSggXqf7LktsxWQ48VAfGwvMrhcVuaZV2fF069c/619RdgCCms0DHhw== -"@patternfly/patternfly@^2.71.6": - version "2.71.6" - resolved "https://registry.yarnpkg.com/@patternfly/patternfly/-/patternfly-2.71.6.tgz#6385cbd5aaca2f59bf65496e0189c541a7f00a82" - integrity sha512-mqqtuCVa+/FbyyK8hSAcfEIwNX73+zbnzHpmC4NrW0kyMzSszPtBqev/ZO79ZxGqZUpLOyUBTVaH7oKn8cL35Q== +"@patternfly/patternfly@^4.10.31": + version "4.10.31" + resolved "https://registry.yarnpkg.com/@patternfly/patternfly/-/patternfly-4.10.31.tgz#742852b69d90bb2efe304130f7226d2e356306cf" + integrity sha512-UxdZ/apWRowXYZ5qPz5LPfXwyB4YGpomrCJPX7c36+Zg8jFpYyVqgVYainL8Yf/GrChtC2LKyoHg7UUTtMtp4A== codemirror@^5.54.0: version "5.54.0" diff --git a/swagger.yaml b/swagger.yaml index aa89db8e0..d36d86d3c 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -5968,7 +5968,7 @@ definitions: x-nullable: true recovery_flow: title: Recovery flow - description: Optional enrollment flow, which is linked at the bottom of the + description: Optional recovery flow, which is linked at the bottom of the page. type: string format: uuid @@ -6079,7 +6079,8 @@ definitions: type: string enum: - text - - e-mail + - username + - email - password - number - checkbox