Compare commits
	
		
			98 Commits
		
	
	
		
			version/0.
			...
			version/0.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 895e7d7393 | |||
| 3beca0574d | |||
| 990f5f0a43 | |||
| 97ce143efe | |||
| cbbe174fd8 | |||
| da3c640343 | |||
| 4b39c71de0 | |||
| 818f417fd8 | |||
| f1ccef7f6a | |||
| 6187436518 | |||
| 9559ee7cb9 | |||
| 72e9c4e6fa | |||
| 97b8a025b3 | |||
| ea9687c30b | |||
| 0a5e14a352 | |||
| 0325847c22 | |||
| 491dcc1159 | |||
| 6292049c74 | |||
| 1e97af772f | |||
| 5c622cd4d2 | |||
| c4de808c4e | |||
| 8c604d225b | |||
| c7daadfb18 | |||
| 683968c96e | |||
| c94added99 | |||
| 61c00e5b39 | |||
| 566ebae065 | |||
| 9b62a6403b | |||
| 8c465b2026 | |||
| 6b7da71aa8 | |||
| e95bbfab9a | |||
| e401575894 | |||
| 6428801270 | |||
| 3e13c13619 | |||
| 92f79eb30e | |||
| e7472de4bf | |||
| 494950ac65 | |||
| 4d51295db2 | |||
| 3bbded3555 | |||
| b3262e2a82 | |||
| 40614a65fc | |||
| 3cf558d594 | |||
| 812cc0d2f1 | |||
| e21ed92848 | |||
| 5184c4b7ef | |||
| 2c07859b68 | |||
| ae6304c05e | |||
| 501683e3cb | |||
| cc8afa8706 | |||
| 17a9e02bc0 | |||
| 6a669992a8 | |||
| 7ea5c22b6c | |||
| b11d6a5891 | |||
| 49830367a7 | |||
| e69ca5a229 | |||
| a57d21f5e8 | |||
| c7026407c6 | |||
| 69eecd6b60 | |||
| 810f10edfe | |||
| 1c57128f11 | |||
| 82eade3eb1 | |||
| 56a9dcc88d | |||
| fe70d80189 | |||
| e97e22c58a | |||
| bb4e39aab6 | |||
| a8744f443c | |||
| 7fe9b8f0b4 | |||
| 696aa7e5f6 | |||
| e1d82aee1d | |||
| 151374f565 | |||
| bebeff9f7f | |||
| 8b99afa34d | |||
| b317852e8a | |||
| 24ae35c35a | |||
| 8e6bb48227 | |||
| 7a4e8af1ae | |||
| 0161205c82 | |||
| ca0ba85023 | |||
| c2ebaa7f64 | |||
| 23cccebb96 | |||
| 3f5d30e6fe | |||
| ca735349f9 | |||
| 25ce8c6dc7 | |||
| 081ac0bcdb | |||
| 8a07b349ee | |||
| b3468bc265 | |||
| 4edfad869f | |||
| 404f5d7912 | |||
| 8bea99a953 | |||
| 0b0ba33dce | |||
| e3627b2cd9 | |||
| 37fac3ae00 | |||
| 17a90adf3e | |||
| 7c3590f8ef | |||
| 7471415e7f | |||
| 9339d496f9 | |||
| e72000eb06 | |||
| ec5ff7c14d | 
| @ -1,5 +1,5 @@ | ||||
| [bumpversion] | ||||
| current_version = 0.10.0-rc1 | ||||
| current_version = 0.10.3-stable | ||||
| tag = True | ||||
| commit = True | ||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*) | ||||
| @ -15,10 +15,10 @@ values = | ||||
| 	beta | ||||
| 	stable | ||||
|  | ||||
| [bumpversion:file:README.md] | ||||
|  | ||||
| [bumpversion:file:docs/installation/docker-compose.md] | ||||
|  | ||||
| [bumpversion:file:docs/installation/kubernetes.md] | ||||
|  | ||||
| [bumpversion:file:docker-compose.yml] | ||||
|  | ||||
| [bumpversion:file:helm/values.yaml] | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| [run] | ||||
| source = passbook | ||||
| relative_files = true | ||||
| omit = | ||||
|     */asgi.py | ||||
|     manage.py | ||||
|  | ||||
							
								
								
									
										34
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										34
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,6 +1,8 @@ | ||||
| name: passbook-release | ||||
| name: passbook-on-release | ||||
|  | ||||
| on: | ||||
|   release | ||||
|   release: | ||||
|     types: [published, created] | ||||
|  | ||||
| jobs: | ||||
|   # Build | ||||
| @ -16,17 +18,26 @@ jobs: | ||||
|       - name: Building Docker Image | ||||
|         run: docker build | ||||
|           --no-cache | ||||
|           -t beryju/passbook:0.10.0-rc1 | ||||
|           -t beryju/passbook:0.10.3-stable | ||||
|           -t beryju/passbook:latest | ||||
|           -f Dockerfile . | ||||
|       - name: Push Docker Container to Registry (versioned) | ||||
|         run: docker push beryju/passbook:0.10.0-rc1 | ||||
|         run: docker push beryju/passbook:0.10.3-stable | ||||
|       - name: Push Docker Container to Registry (latest) | ||||
|         run: docker push beryju/passbook:latest | ||||
|   build-proxy: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v1 | ||||
|       - uses: actions/setup-go@v2 | ||||
|         with: | ||||
|           go-version: "^1.15" | ||||
|       - name: prepare go api client | ||||
|         run: | | ||||
|           cd proxy | ||||
|           go get -u github.com/go-swagger/go-swagger/cmd/swagger | ||||
|           swagger generate client -f ../swagger.yaml -A passbook -t pkg/ | ||||
|           go build -v . | ||||
|       - name: Docker Login Registry | ||||
|         env: | ||||
|           DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} | ||||
| @ -37,11 +48,11 @@ jobs: | ||||
|           cd proxy | ||||
|           docker build \ | ||||
|           --no-cache \ | ||||
|           -t beryju/passbook-proxy:0.10.0-rc1 \ | ||||
|           -t beryju/passbook-proxy:0.10.3-stable \ | ||||
|           -t beryju/passbook-proxy:latest \ | ||||
|           -f Dockerfile . | ||||
|       - name: Push Docker Container to Registry (versioned) | ||||
|         run: docker push beryju/passbook-proxy:0.10.0-rc1 | ||||
|         run: docker push beryju/passbook-proxy:0.10.3-stable | ||||
|       - name: Push Docker Container to Registry (latest) | ||||
|         run: docker push beryju/passbook-proxy:latest | ||||
|   build-static: | ||||
| @ -66,11 +77,11 @@ jobs: | ||||
|         run: docker build | ||||
|           --no-cache | ||||
|           --network=$(docker network ls | grep github | awk '{print $1}') | ||||
|           -t beryju/passbook-static:0.10.0-rc1 | ||||
|           -t beryju/passbook-static:0.10.3-stable | ||||
|           -t beryju/passbook-static:latest | ||||
|           -f static.Dockerfile . | ||||
|       - name: Push Docker Container to Registry (versioned) | ||||
|         run: docker push beryju/passbook-static:0.10.0-rc1 | ||||
|         run: docker push beryju/passbook-static:0.10.3-stable | ||||
|       - name: Push Docker Container to Registry (latest) | ||||
|         run: docker push beryju/passbook-static:latest | ||||
|   test-release: | ||||
| @ -82,10 +93,13 @@ jobs: | ||||
|       - uses: actions/checkout@v1 | ||||
|       - name: Run test suite in final docker images | ||||
|         run: | | ||||
|           sudo apt-get install -y pwgen | ||||
|           echo "PG_PASS=$(pwgen 40 1)" >> .env | ||||
|           echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env | ||||
|           docker-compose pull -q | ||||
|           docker-compose up --no-start | ||||
|           docker-compose start postgresql redis | ||||
|           docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test" | ||||
|           docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test passbook" | ||||
|   sentry-release: | ||||
|     needs: | ||||
|       - test-release | ||||
| @ -100,5 +114,5 @@ jobs: | ||||
|           SENTRY_PROJECT: passbook | ||||
|           SENTRY_URL: https://sentry.beryju.org | ||||
|         with: | ||||
|           tagName: 0.10.0-rc1 | ||||
|           tagName: 0.10.3-stable | ||||
|           environment: beryjuorg-prod | ||||
|  | ||||
							
								
								
									
										14
									
								
								.github/workflows/tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/tag.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,10 +1,10 @@ | ||||
| name: passbook-on-tag | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     tags: | ||||
|     - 'version/*' | ||||
|  | ||||
| name: passbook-version-tag | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     name: Create Release from Tag | ||||
| @ -13,6 +13,10 @@ jobs: | ||||
|       - uses: actions/checkout@master | ||||
|       - name: Pre-release test | ||||
|         run: | | ||||
|           sudo apt-get install -y pwgen | ||||
|           echo "PASSBOOK_TAG=latest" >> .env | ||||
|           echo "PG_PASS=$(pwgen 40 1)" >> .env | ||||
|           echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env | ||||
|           docker-compose pull -q | ||||
|           docker build \ | ||||
|             --no-cache \ | ||||
| @ -20,7 +24,7 @@ jobs: | ||||
|             -f Dockerfile . | ||||
|           docker-compose up --no-start | ||||
|           docker-compose start postgresql redis | ||||
|           docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test" | ||||
|           docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test passbook" | ||||
|       - name: Install Helm | ||||
|         run: | | ||||
|           apt update && apt install -y curl | ||||
| @ -30,7 +34,7 @@ jobs: | ||||
|           helm dependency update helm/ | ||||
|           helm package helm/ | ||||
|           mv passbook-*.tgz passbook-chart.tgz | ||||
|       - name: Extract verison number | ||||
|       - name: Extract version number | ||||
|         id: get_version | ||||
|         uses: actions/github-script@0.2.0 | ||||
|         with: | ||||
| @ -45,7 +49,7 @@ jobs: | ||||
|         with: | ||||
|           tag_name: ${{ github.ref }} | ||||
|           release_name: Release ${{ steps.get_version.outputs.result }} | ||||
|           draft: false | ||||
|           draft: true | ||||
|           prerelease: false | ||||
|       - name: Upload packaged Helm Chart | ||||
|         id: upload-release-asset | ||||
|  | ||||
							
								
								
									
										8
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								Makefile
									
									
									
									
									
								
							| @ -1,7 +1,7 @@ | ||||
| all: lint-fix lint coverage gen | ||||
|  | ||||
| coverage: | ||||
| 	coverage run --concurrency=multiprocessing manage.py test passbook --failfast | ||||
| 	coverage run --concurrency=multiprocessing manage.py test --failfast -v 3 | ||||
| 	coverage combine | ||||
| 	coverage html | ||||
| 	coverage report | ||||
| @ -18,3 +18,9 @@ lint: | ||||
|  | ||||
| gen: coverage | ||||
| 	./manage.py generate_swagger -o swagger.yaml -f yaml | ||||
|  | ||||
| local-stack: | ||||
| 	export PASSBOOK_TAG=testing | ||||
| 	docker build -t beryju/passbook:testng . | ||||
| 	docker-compose up -d | ||||
| 	docker-compose run --rm server migrate | ||||
|  | ||||
							
								
								
									
										3
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								Pipfile
									
									
									
									
									
								
							| @ -59,5 +59,6 @@ docker = "*" | ||||
| pylint = "*" | ||||
| pylint-django = "*" | ||||
| selenium = "*" | ||||
| unittest-xml-reporting = "*" | ||||
| prospector = "*" | ||||
| pytest = "*" | ||||
| pytest-django = "*" | ||||
|  | ||||
							
								
								
									
										244
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										244
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @ -1,7 +1,7 @@ | ||||
| { | ||||
|     "_meta": { | ||||
|         "hash": { | ||||
|             "sha256": "a798bbd0b97857cac136c1743b8d6ad8bf8c3d95e2760c71d324bb2a7f47f678" | ||||
|             "sha256": "80570636236962f4b934a884817292de9f7bb48520aa964afc2959b0f795fb57" | ||||
|         }, | ||||
|         "pipfile-spec": 6, | ||||
|         "requires": { | ||||
| @ -74,18 +74,17 @@ | ||||
|         }, | ||||
|         "boto3": { | ||||
|             "hashes": [ | ||||
|                 "sha256:2ab73b0c400ab8c7df84bee7564ef8a0813021da28dd7a05fcbffb77a8ae9de9", | ||||
|                 "sha256:bb2222fa02fcd09b39e581e532d4f013ea850742d8cd46e9c10a21028b6d2ef5" | ||||
|                 "sha256:25c716b7c01d4664027afc6a6418a06459e311a610c7fd39a030a1ced1b72ce4" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.14.56" | ||||
|             "version": "==1.14.63" | ||||
|         }, | ||||
|         "botocore": { | ||||
|             "hashes": [ | ||||
|                 "sha256:37cc3f1013c00dc0f061582198d6b785dadf147bd99307d41c5c0e47debca65c", | ||||
|                 "sha256:acd2df778a5e12b2a16ac040ce6e91a6c6f2d7ac67bd4f966472ce5c68b5b62d" | ||||
|                 "sha256:40f13f6c9c29c307a9dc5982739e537ddce55b29787b90c3447b507e3283bcd6", | ||||
|                 "sha256:aa88eafc6295132f4bc606f1df32b3248e0fa611724c0a216aceda767948ac75" | ||||
|             ], | ||||
|             "version": "==1.17.58" | ||||
|             "version": "==1.17.63" | ||||
|         }, | ||||
|         "cachetools": { | ||||
|             "hashes": [ | ||||
| @ -111,36 +110,44 @@ | ||||
|         }, | ||||
|         "cffi": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e", | ||||
|                 "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c", | ||||
|                 "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e", | ||||
|                 "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1", | ||||
|                 "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4", | ||||
|                 "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2", | ||||
|                 "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c", | ||||
|                 "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0", | ||||
|                 "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798", | ||||
|                 "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1", | ||||
|                 "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4", | ||||
|                 "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731", | ||||
|                 "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4", | ||||
|                 "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c", | ||||
|                 "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487", | ||||
|                 "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e", | ||||
|                 "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f", | ||||
|                 "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123", | ||||
|                 "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c", | ||||
|                 "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b", | ||||
|                 "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650", | ||||
|                 "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad", | ||||
|                 "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75", | ||||
|                 "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82", | ||||
|                 "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7", | ||||
|                 "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15", | ||||
|                 "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa", | ||||
|                 "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281" | ||||
|                 "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", | ||||
|                 "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", | ||||
|                 "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", | ||||
|                 "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", | ||||
|                 "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", | ||||
|                 "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", | ||||
|                 "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", | ||||
|                 "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", | ||||
|                 "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", | ||||
|                 "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", | ||||
|                 "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", | ||||
|                 "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", | ||||
|                 "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", | ||||
|                 "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", | ||||
|                 "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", | ||||
|                 "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", | ||||
|                 "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", | ||||
|                 "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", | ||||
|                 "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", | ||||
|                 "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", | ||||
|                 "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", | ||||
|                 "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", | ||||
|                 "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", | ||||
|                 "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", | ||||
|                 "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", | ||||
|                 "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", | ||||
|                 "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", | ||||
|                 "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", | ||||
|                 "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", | ||||
|                 "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", | ||||
|                 "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", | ||||
|                 "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", | ||||
|                 "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", | ||||
|                 "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", | ||||
|                 "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", | ||||
|                 "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" | ||||
|             ], | ||||
|             "version": "==1.14.2" | ||||
|             "version": "==1.14.3" | ||||
|         }, | ||||
|         "channels": { | ||||
|             "hashes": [ | ||||
| @ -327,11 +334,11 @@ | ||||
|         }, | ||||
|         "django-storages": { | ||||
|             "hashes": [ | ||||
|                 "sha256:1e37da57678e6cf1e9914f84099a305323e4e1f261afe54fdb703cae7aa6fbc3", | ||||
|                 "sha256:36ed8dab33d761954498189592ce005920095fcbc02dab4184eb51393c370991" | ||||
|                 "sha256:12de8fb2605b9b57bfaf54b075280d7cbb3b3ee1ca4bc9b9add147af87fe3a2c", | ||||
|                 "sha256:652275ab7844538c462b62810276c0244866f345878256a9e0e86f5b1283ae18" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.10" | ||||
|             "version": "==1.10.1" | ||||
|         }, | ||||
|         "djangorestframework": { | ||||
|             "hashes": [ | ||||
| @ -387,10 +394,10 @@ | ||||
|         }, | ||||
|         "google-auth": { | ||||
|             "hashes": [ | ||||
|                 "sha256:bcbd9f970e7144fe933908aa286d7a12c44b7deb6d78a76871f0377a29d09789", | ||||
|                 "sha256:f4d5093f13b1b1c0a434ab1dc851cd26a983f86a4d75c95239974e33ed406a87" | ||||
|                 "sha256:7084c50c03f7a8a5696ef4500e65df0c525a0f6909f3c70b9ee65900a230c755", | ||||
|                 "sha256:dcf86c5adc3a8a7659be190b12bb8912ae019cfd9ee2a571ea881e289fafbe39" | ||||
|             ], | ||||
|             "version": "==1.21.1" | ||||
|             "version": "==1.21.2" | ||||
|         }, | ||||
|         "gunicorn": { | ||||
|             "hashes": [ | ||||
| @ -835,9 +842,9 @@ | ||||
|         }, | ||||
|         "pyrsistent": { | ||||
|             "hashes": [ | ||||
|                 "sha256:27515d2d5db0629c7dadf6fbe76973eb56f098c1b01d36de42eb69220d2c19e4" | ||||
|                 "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e" | ||||
|             ], | ||||
|             "version": "==0.17.2" | ||||
|             "version": "==0.17.3" | ||||
|         }, | ||||
|         "python-dateutil": { | ||||
|             "hashes": [ | ||||
| @ -952,11 +959,11 @@ | ||||
|         }, | ||||
|         "sentry-sdk": { | ||||
|             "hashes": [ | ||||
|                 "sha256:97bff68e57402ad39674e6fe2545df0d5eea41c3d51e280c170761705c8c20ff", | ||||
|                 "sha256:a16caf9ce892623081cbb9a95f6c1f892778bb123909b0ed7afdfb52ce7a58a1" | ||||
|                 "sha256:1a086486ff9da15791f294f6e9915eb3747d161ef64dee2d038a4d0b4a369b24", | ||||
|                 "sha256:45486deb031cea6bbb25a540d7adb4dd48cd8a1cc31e6a5ce9fb4f792a572e9a" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.17.4" | ||||
|             "version": "==0.17.6" | ||||
|         }, | ||||
|         "service-identity": { | ||||
|             "hashes": [ | ||||
| @ -1269,43 +1276,43 @@ | ||||
|         }, | ||||
|         "coverage": { | ||||
|             "hashes": [ | ||||
|                 "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", | ||||
|                 "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", | ||||
|                 "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", | ||||
|                 "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", | ||||
|                 "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", | ||||
|                 "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", | ||||
|                 "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", | ||||
|                 "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", | ||||
|                 "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", | ||||
|                 "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", | ||||
|                 "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", | ||||
|                 "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", | ||||
|                 "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", | ||||
|                 "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", | ||||
|                 "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", | ||||
|                 "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", | ||||
|                 "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", | ||||
|                 "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", | ||||
|                 "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", | ||||
|                 "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", | ||||
|                 "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", | ||||
|                 "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", | ||||
|                 "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", | ||||
|                 "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", | ||||
|                 "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", | ||||
|                 "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", | ||||
|                 "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", | ||||
|                 "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", | ||||
|                 "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", | ||||
|                 "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", | ||||
|                 "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", | ||||
|                 "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", | ||||
|                 "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", | ||||
|                 "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" | ||||
|                 "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", | ||||
|                 "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", | ||||
|                 "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", | ||||
|                 "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", | ||||
|                 "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", | ||||
|                 "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", | ||||
|                 "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", | ||||
|                 "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", | ||||
|                 "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", | ||||
|                 "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", | ||||
|                 "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", | ||||
|                 "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", | ||||
|                 "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", | ||||
|                 "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", | ||||
|                 "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", | ||||
|                 "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", | ||||
|                 "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", | ||||
|                 "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", | ||||
|                 "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", | ||||
|                 "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", | ||||
|                 "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", | ||||
|                 "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", | ||||
|                 "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", | ||||
|                 "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", | ||||
|                 "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", | ||||
|                 "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", | ||||
|                 "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", | ||||
|                 "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", | ||||
|                 "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", | ||||
|                 "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", | ||||
|                 "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", | ||||
|                 "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", | ||||
|                 "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", | ||||
|                 "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==5.2.1" | ||||
|             "version": "==5.3" | ||||
|         }, | ||||
|         "django": { | ||||
|             "hashes": [ | ||||
| @ -1373,6 +1380,13 @@ | ||||
|             ], | ||||
|             "version": "==2.10" | ||||
|         }, | ||||
|         "iniconfig": { | ||||
|             "hashes": [ | ||||
|                 "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", | ||||
|                 "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69" | ||||
|             ], | ||||
|             "version": "==1.0.1" | ||||
|         }, | ||||
|         "isort": { | ||||
|             "hashes": [ | ||||
|                 "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", | ||||
| @ -1413,6 +1427,21 @@ | ||||
|             ], | ||||
|             "version": "==0.6.1" | ||||
|         }, | ||||
|         "more-itertools": { | ||||
|             "hashes": [ | ||||
|                 "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20", | ||||
|                 "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c" | ||||
|             ], | ||||
|             "version": "==8.5.0" | ||||
|         }, | ||||
|         "packaging": { | ||||
|             "hashes": [ | ||||
|                 "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", | ||||
|                 "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==20.4" | ||||
|         }, | ||||
|         "pathspec": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", | ||||
| @ -1434,6 +1463,13 @@ | ||||
|             ], | ||||
|             "version": "==0.10.0" | ||||
|         }, | ||||
|         "pluggy": { | ||||
|             "hashes": [ | ||||
|                 "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", | ||||
|                 "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" | ||||
|             ], | ||||
|             "version": "==0.13.1" | ||||
|         }, | ||||
|         "prospector": { | ||||
|             "hashes": [ | ||||
|                 "sha256:43e5e187c027336b0e4c4aa6a82d66d3b923b5ec5b51968126132e32f9d14a2f" | ||||
| @ -1441,6 +1477,13 @@ | ||||
|             "index": "pypi", | ||||
|             "version": "==1.3.0" | ||||
|         }, | ||||
|         "py": { | ||||
|             "hashes": [ | ||||
|                 "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", | ||||
|                 "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" | ||||
|             ], | ||||
|             "version": "==1.9.0" | ||||
|         }, | ||||
|         "pycodestyle": { | ||||
|             "hashes": [ | ||||
|                 "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", | ||||
| @ -1497,6 +1540,29 @@ | ||||
|             ], | ||||
|             "version": "==0.6" | ||||
|         }, | ||||
|         "pyparsing": { | ||||
|             "hashes": [ | ||||
|                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", | ||||
|                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" | ||||
|             ], | ||||
|             "version": "==2.4.7" | ||||
|         }, | ||||
|         "pytest": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40", | ||||
|                 "sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==6.0.2" | ||||
|         }, | ||||
|         "pytest-django": { | ||||
|             "hashes": [ | ||||
|                 "sha256:4de6dbd077ed8606616958f77655fed0d5e3ee45159475671c7fa67596c6dba6", | ||||
|                 "sha256:c33e3d3da14d8409b125d825d4e74da17bb252191bf6fc3da6856e27a8b73ea4" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.10.0" | ||||
|         }, | ||||
|         "pytz": { | ||||
|             "hashes": [ | ||||
|                 "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", | ||||
| @ -1604,10 +1670,10 @@ | ||||
|         }, | ||||
|         "stevedore": { | ||||
|             "hashes": [ | ||||
|                 "sha256:a34086819e2c7a7f86d5635363632829dab8014e5fd7be2454c7cba84ac7514e", | ||||
|                 "sha256:ddc09a744dc224c84ec8e8efcb70595042d21c97c76df60daee64c9ad53bc7ee" | ||||
|                 "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62", | ||||
|                 "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0" | ||||
|             ], | ||||
|             "version": "==3.2.1" | ||||
|             "version": "==3.2.2" | ||||
|         }, | ||||
|         "toml": { | ||||
|             "hashes": [ | ||||
| @ -1642,14 +1708,6 @@ | ||||
|             ], | ||||
|             "version": "==1.4.1" | ||||
|         }, | ||||
|         "unittest-xml-reporting": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7bf515ea8cb244255a25100cd29db611a73f8d3d0aaf672ed3266307e14cc1ca", | ||||
|                 "sha256:984cebba69e889401bfe3adb9088ca376b3a1f923f0590d005126c1bffd1a695" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.0.4" | ||||
|         }, | ||||
|         "urllib3": { | ||||
|             "extras": [ | ||||
|                 "secure" | ||||
|  | ||||
							
								
								
									
										17
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								README.md
									
									
									
									
									
								
							| @ -1,4 +1,4 @@ | ||||
| <img src="passbook/static/static/passbook/logo.svg" height="50" alt="passbook logo"><img src="passbook/static/static/passbook/brand_inverted.svg" height="50" alt="passbook"> | ||||
| <img src="docs/images/logo.svg" height="50" alt="passbook logo"><img src="docs/images/brand_inverted.svg" height="50" alt="passbook"> | ||||
|  | ||||
| [](https://dev.azure.com/beryjuorg/passbook/_build?definitionId=1) | ||||
|  | ||||
| @ -13,20 +13,7 @@ passbook is an open-source Identity Provider focused on flexibility and versatil | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| For small/test setups it is recommended to use docker-compose. | ||||
|  | ||||
| ``` | ||||
| wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml | ||||
| # Optionally enable Error-reporting | ||||
| # export PASSBOOK_ERROR_REPORTING=true | ||||
| # Optionally deploy a different version | ||||
| # export PASSBOOK_TAG=0.10.0-rc1 | ||||
| # If this is a productive installation, set a different PostgreSQL Password | ||||
| # export PG_PASS=$(pwgen 40 1) | ||||
| docker-compose pull | ||||
| docker-compose up -d | ||||
| docker-compose run --rm server migrate | ||||
| ``` | ||||
| For small/test setups it is recommended to use docker-compose, see the [documentation](https://passbook.beryju.org/installation/docker-compose/) | ||||
|  | ||||
| For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://passbook.beryju.org//installation/kubernetes/) | ||||
|  | ||||
|  | ||||
| @ -139,7 +139,7 @@ stages: | ||||
|             displayName: Run full test suite | ||||
|             inputs: | ||||
|               script: | | ||||
|                 pipenv run coverage run ./manage.py test passbook | ||||
|                 pipenv run coverage run ./manage.py test passbook -v 3 | ||||
|                 mkdir output-unittest | ||||
|                 mv unittest.xml output-unittest/unittest.xml | ||||
|                 mv .coverage output-unittest/coverage | ||||
| @ -150,7 +150,7 @@ stages: | ||||
|               publishLocation: 'pipeline' | ||||
|       - job: coverage_e2e | ||||
|         pool: | ||||
|           vmImage: 'ubuntu-latest' | ||||
|           name: coventry | ||||
|         steps: | ||||
|           - task: UsePythonVersion@0 | ||||
|             inputs: | ||||
| @ -181,7 +181,14 @@ stages: | ||||
|           - task: CmdLine@2 | ||||
|             displayName: Run full test suite | ||||
|             inputs: | ||||
|               script: pipenv run coverage run ./manage.py test e2e | ||||
|               script: | | ||||
|                 pipenv run coverage run ./manage.py test e2e -v 3 | ||||
|           - task: CmdLine@2 | ||||
|             condition: always() | ||||
|             displayName: Cleanup | ||||
|             inputs: | ||||
|               script: | | ||||
|                 docker stop $(docker ps -aq) | ||||
|           - task: CmdLine@2 | ||||
|             displayName: Prepare unittests and coverage for upload | ||||
|             inputs: | ||||
| @ -225,11 +232,9 @@ stages: | ||||
|               script: | | ||||
|                 sudo pip install -U wheel pipenv | ||||
|                 pipenv install --dev | ||||
|                 find . | ||||
|                 pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage | ||||
|                 pipenv run coverage xml | ||||
|                 pipenv run coverage html | ||||
|                 find . | ||||
|           - task: PublishCodeCoverageResults@1 | ||||
|             inputs: | ||||
|               codeCoverageTool: 'Cobertura' | ||||
|  | ||||
| @ -14,6 +14,8 @@ services: | ||||
|       - POSTGRES_DB=passbook | ||||
|     labels: | ||||
|       - traefik.enable=false | ||||
|     env_file: | ||||
|       - .env | ||||
|   redis: | ||||
|     image: redis | ||||
|     networks: | ||||
| @ -21,13 +23,12 @@ services: | ||||
|     labels: | ||||
|       - traefik.enable=false | ||||
|   server: | ||||
|     image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-rc1} | ||||
|     image: beryju/passbook:${PASSBOOK_TAG:-0.10.3-stable} | ||||
|     command: server | ||||
|     environment: | ||||
|       PASSBOOK_REDIS__HOST: redis | ||||
|       PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false} | ||||
|       PASSBOOK_POSTGRESQL__HOST: postgresql | ||||
|       PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword} | ||||
|       PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS} | ||||
|       PASSBOOK_LOG_LEVEL: debug | ||||
|     ports: | ||||
|       - 8000 | ||||
| @ -37,8 +38,10 @@ services: | ||||
|       - traefik.port=8000 | ||||
|       - traefik.docker.network=internal | ||||
|       - traefik.frontend.rule=PathPrefix:/ | ||||
|     env_file: | ||||
|       - .env | ||||
|   worker: | ||||
|     image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-rc1} | ||||
|     image: beryju/passbook:${PASSBOOK_TAG:-0.10.3-stable} | ||||
|     command: worker | ||||
|     networks: | ||||
|       - internal | ||||
| @ -46,12 +49,13 @@ services: | ||||
|       - traefik.enable=false | ||||
|     environment: | ||||
|       PASSBOOK_REDIS__HOST: redis | ||||
|       PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false} | ||||
|       PASSBOOK_POSTGRESQL__HOST: postgresql | ||||
|       PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword} | ||||
|       PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS} | ||||
|       PASSBOOK_LOG_LEVEL: debug | ||||
|     env_file: | ||||
|       - .env | ||||
|   static: | ||||
|     image: beryju/passbook-static:0.10.0-rc1 | ||||
|     image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.3-stable} | ||||
|     networks: | ||||
|       - internal | ||||
|     labels: | ||||
|  | ||||
| @ -11,14 +11,21 @@ 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. | ||||
|  | ||||
| To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING=true >> .env` | ||||
|  | ||||
| To optionally deploy a different version run `echo PASSBOOK_TAG=0.10.3-stable >> .env` | ||||
|  | ||||
| If this is a fresh passbook install run the following commands to generate a password: | ||||
|  | ||||
| ``` | ||||
| sudo apt-get install -y pwgen | ||||
| echo "PG_PASS=$(pwgen 40 1)" >> .env | ||||
| echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env | ||||
| ``` | ||||
|  | ||||
| Afterwards, run these commands to finish | ||||
|  | ||||
| ``` | ||||
| wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml | ||||
| # Optionally enable Error-reporting | ||||
| # export PASSBOOK_ERROR_REPORTING=true | ||||
| # Optionally deploy a different version | ||||
| # export PASSBOOK_TAG=0.10.0-rc1 | ||||
| # If this is a productive installation, set a different PostgreSQL Password | ||||
| # export PG_PASS=$(pwgen 40 1) | ||||
| docker-compose pull | ||||
| docker-compose up -d | ||||
| docker-compose run --rm server migrate | ||||
|  | ||||
| @ -11,7 +11,7 @@ This installation automatically applies database migrations on startup. After th | ||||
| image: | ||||
|   name: beryju/passbook | ||||
|   name_static: beryju/passbook-static | ||||
|   tag: 0.9.0-stable | ||||
|   tag: 0.10.3-stable | ||||
|  | ||||
| nameOverride: "" | ||||
|  | ||||
|  | ||||
							
								
								
									
										77
									
								
								docs/integrations/services/vmware-vcenter/index.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								docs/integrations/services/vmware-vcenter/index.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,77 @@ | ||||
| # VMware vCenter Integration | ||||
|  | ||||
| ## What is vCenter | ||||
|  | ||||
| From https://en.wikipedia.org/wiki/VCenter | ||||
|  | ||||
| !!! note "" | ||||
|  | ||||
|     vCenter Server is the centralized management utility for VMware, and is used to manage virtual machines, multiple ESXi hosts, and all dependent components from a single centralized location. VMware vMotion and svMotion require the use of vCenter and ESXi hosts. | ||||
|  | ||||
| !!! warning | ||||
|  | ||||
|     This requires passbook 0.10.3 or newer. | ||||
|  | ||||
| !!! warning | ||||
|  | ||||
|     This requires VMware vCenter 7.0.0 or newer. | ||||
|  | ||||
| ## Preparation | ||||
|  | ||||
| The following placeholders will be used: | ||||
|  | ||||
|  - `vcenter.company` is the FQDN of the vCenter server. | ||||
|  - `passbook.company` is the FQDN of the passbook install. | ||||
|  | ||||
| Since vCenter only allows OpenID-Connect in combination with Active Directory, it is recommended to have passbook sync with the same Active Directory. | ||||
|  | ||||
| ### Step 1 | ||||
|  | ||||
| Under *Property Mappings*, create a *Scope Mapping*. Give it a name like "OIDC-Scope-VMware-vCenter". Set the scope name to `openid` and the expression to the following | ||||
|  | ||||
| ```python | ||||
| return { | ||||
|   "domain": "<your active directory domain>", | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Step 2 | ||||
|  | ||||
| !!! note | ||||
|     If your Active Directory Schema is the same as your Email address schema, skip to Step 3. | ||||
|  | ||||
| Under *Sources*, click *Edit* and ensure that "Autogenerated Active Directory Mapping: userPrincipalName -> attributes.upn" has been added to your source. | ||||
|  | ||||
| ### Step 3 | ||||
|  | ||||
| Under *Providers*, create an OAuth2/OpenID Provider with these settings: | ||||
|  | ||||
|  - Client Type: Confidential | ||||
|  - Response Type: code | ||||
|  - JWT Algorithm: RS256 | ||||
|  - Redirect URI: `https://vcenter.company/ui/login/oauth2/authcode` | ||||
|  - Post Logout Redirect URIs: `https://vcenter.company/ui/login` | ||||
|  - Sub Mode: If your Email address Schema matches your UPN, select "Based on the User's Email...", otherwise select "Based on the User's UPN...". | ||||
|  - Scopes: Select the Scope Mapping you've created in Step 1 | ||||
|  | ||||
|  | ||||
|  | ||||
| ### Step 4 | ||||
|  | ||||
| Create an application which uses this provider. Optionally apply access restrictions to the application. | ||||
|  | ||||
| ## vCenter Setup | ||||
|  | ||||
| Login as local Administrator account (most likely ends with vsphere.local). Using the Menu in the Navigation bar, navigate to *Administration -> Single Sing-on -> Configuration*. | ||||
|  | ||||
| Click on *Change Identity Provider* in the top-right corner. | ||||
|  | ||||
| In the wizard, select "Microsoft ADFS" and click Next. | ||||
|  | ||||
| Fill in the Client Identifier and Shared Secret from the Provider in passbook. For the OpenID Address, click on *View Setup URLs* in passbook, and copy the OpenID Configuration URL. | ||||
|  | ||||
| On the next page, fill in your Active Directory Connection Details. These should be similar to what you have set in passbook. | ||||
|  | ||||
|  | ||||
|  | ||||
| If your vCenter was already setup with LDAP beforehand, your Role assignments will continue to work. | ||||
							
								
								
									
										
											BIN
										
									
								
								docs/integrations/services/vmware-vcenter/passbook_setup.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/integrations/services/vmware-vcenter/passbook_setup.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 173 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/integrations/services/vmware-vcenter/vcenter_post_setup.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/integrations/services/vmware-vcenter/vcenter_post_setup.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 89 KiB | 
							
								
								
									
										20
									
								
								docs/outposts/deploy-docker-compose.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								docs/outposts/deploy-docker-compose.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| # Outpost deployment in docker-compose | ||||
|  | ||||
| To deploy an outpost with docker-compose, use  this snippet in your docker-compose file. | ||||
|  | ||||
| You can also run the outpost in a separate docker-compose project, you just have to ensure that the outpost container can reach your application container. | ||||
|  | ||||
| ```yaml | ||||
| version: 3.5 | ||||
|  | ||||
| services: | ||||
|   passbook_proxy: | ||||
|     image: beryju/passbook-proxy:0.10.0-stable | ||||
|     ports: | ||||
|       - 4180:4180 | ||||
|       - 4443:4443 | ||||
|     environment: | ||||
|       PASSBOOK_HOST: https://your-passbook.tld | ||||
|       PASSBOOK_INSECURE: 'true' | ||||
|       PASSBOOK_TOKEN: token-generated-by-passbook | ||||
| ``` | ||||
							
								
								
									
										99
									
								
								docs/outposts/deploy-kubernetes.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								docs/outposts/deploy-kubernetes.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,99 @@ | ||||
| # Outpost deployment on Kubernetes | ||||
|  | ||||
| Use the following manifest, replacing all values surrounded with `__`. | ||||
|  | ||||
| Afterwards, configure the proxy provider to connect to `<service name>.<namespace>.svc.cluster.local`, and update your Ingress to connect to the `passbook-outpost` service. | ||||
|  | ||||
| ```yaml | ||||
| apiVersion: v1 | ||||
| kind: Secret | ||||
| metadata: | ||||
|   labels: | ||||
|     app.kubernetes.io/instance: test | ||||
|     app.kubernetes.io/managed-by: passbook.beryju.org | ||||
|     app.kubernetes.io/name: passbook-proxy | ||||
|     app.kubernetes.io/version: 0.10.0 | ||||
|   name: passbook-outpost-api | ||||
| stringData: | ||||
|   passbook_host: '__PASSBOOK_URL__' | ||||
|   passbook_host_insecure: 'true' | ||||
|   token: '__PASSBOOK_TOKEN__' | ||||
| type: Opaque | ||||
| --- | ||||
| apiVersion: v1 | ||||
| kind: Service | ||||
| metadata: | ||||
|   labels: | ||||
|     app.kubernetes.io/instance: test | ||||
|     app.kubernetes.io/managed-by: passbook.beryju.org | ||||
|     app.kubernetes.io/name: passbook-proxy | ||||
|     app.kubernetes.io/version: 0.10.0 | ||||
|   name: passbook-outpost | ||||
| spec: | ||||
|   ports: | ||||
|   - name: http | ||||
|     port: 4180 | ||||
|     protocol: TCP | ||||
|     targetPort: http | ||||
|   - name: https | ||||
|     port: 4443 | ||||
|     protocol: TCP | ||||
|     targetPort: https | ||||
|   selector: | ||||
|     app.kubernetes.io/instance: test | ||||
|     app.kubernetes.io/managed-by: passbook.beryju.org | ||||
|     app.kubernetes.io/name: passbook-proxy | ||||
|     app.kubernetes.io/version: 0.10.0 | ||||
|   type: ClusterIP | ||||
| --- | ||||
| apiVersion: apps/v1 | ||||
| kind: Deployment | ||||
| metadata: | ||||
|   labels: | ||||
|     app.kubernetes.io/instance: test | ||||
|     app.kubernetes.io/managed-by: passbook.beryju.org | ||||
|     app.kubernetes.io/name: passbook-proxy | ||||
|     app.kubernetes.io/version: 0.10.0 | ||||
|   name: passbook-outpost | ||||
| spec: | ||||
|   selector: | ||||
|     matchLabels: | ||||
|       app.kubernetes.io/instance: test | ||||
|       app.kubernetes.io/managed-by: passbook.beryju.org | ||||
|       app.kubernetes.io/name: passbook-proxy | ||||
|       app.kubernetes.io/version: 0.10.0 | ||||
|   template: | ||||
|     metadata: | ||||
|       labels: | ||||
|         app.kubernetes.io/instance: test | ||||
|         app.kubernetes.io/managed-by: passbook.beryju.org | ||||
|         app.kubernetes.io/name: passbook-proxy | ||||
|         app.kubernetes.io/version: 0.10.0 | ||||
|     spec: | ||||
|       containers: | ||||
|       - env: | ||||
|         - name: PASSBOOK_HOST | ||||
|           valueFrom: | ||||
|             secretKeyRef: | ||||
|               key: passbook_host | ||||
|               name: passbook-outpost-api | ||||
|         - name: PASSBOOK_TOKEN | ||||
|           valueFrom: | ||||
|             secretKeyRef: | ||||
|               key: token | ||||
|               name: passbook-outpost-api | ||||
|         - name: PASSBOOK_INSECURE | ||||
|           valueFrom: | ||||
|             secretKeyRef: | ||||
|               key: passbook_host_insecure | ||||
|               name: passbook-outpost-api | ||||
|         image: beryju/passbook-proxy:0.10.0-stable | ||||
|         name: proxy | ||||
|         ports: | ||||
|         - containerPort: 4180 | ||||
|           name: http | ||||
|           protocol: TCP | ||||
|         - containerPort: 4443 | ||||
|           name: https | ||||
|           protocol: TCP | ||||
| ``` | ||||
| @ -6,21 +6,9 @@ An outpost is a single deployment of a passbook component, which can be deployed | ||||
|  | ||||
| Upon creation, a service account and a token is generated. The service account only has permissions to read the outpost and provider configuration. This token is used by the Outpost to connect to passbook. | ||||
|  | ||||
| To deploy an outpost, you can for example use this docker-compose snippet: | ||||
| To deploy an outpost, see: <a name="deploy"> | ||||
|  | ||||
| ```yaml | ||||
| version: 3.5 | ||||
| - [Kubernetes](deploy-kubernetes.md) | ||||
| - [docker-compose](deploy-docker-compose.md) | ||||
|  | ||||
| services: | ||||
|   passbook_proxy: | ||||
|     image: beryju/passbook-proxy:0.10.0-stable | ||||
|     ports: | ||||
|       - 4180:4180 | ||||
|       - 4443:4443 | ||||
|     environment: | ||||
|       PASSBOOK_HOST: https://your-passbook.tld | ||||
|       PASSBOOK_INSECURE: 'true' | ||||
|       PASSBOOK_TOKEN: token-generated-by-passbook | ||||
| ``` | ||||
|  | ||||
| In future versions, this snippet will be automatically generated. You will also be able to deploy an outpost directly into a kubernetes cluster.w | ||||
| In future versions, this snippet will be automatically generated. You will also be able to deploy an outpost directly into a kubernetes cluster. | ||||
|  | ||||
							
								
								
									
										11
									
								
								docs/troubleshooting/access.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								docs/troubleshooting/access.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| # Troubleshooting access problems | ||||
|  | ||||
| ## I get an access denied error when trying to access an application. | ||||
|  | ||||
| If your user is a superuser, or has the attribute `passbook_user_debug` set to true: | ||||
|  | ||||
|  | ||||
|  | ||||
| Afterwards, try to access the application again. You will now see a message explaining which policy denied you access: | ||||
|  | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								docs/troubleshooting/access_denied_message.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/troubleshooting/access_denied_message.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 98 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/troubleshooting/passbook_user_debug.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/troubleshooting/passbook_user_debug.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 13 KiB | 
| @ -2,7 +2,7 @@ version: '3.7' | ||||
|  | ||||
| services: | ||||
|   chrome: | ||||
|     image: selenium/standalone-chrome-debug:3.141.59-20200525 | ||||
|     image: selenium/standalone-chrome-debug:3.141.59-20200719 | ||||
|     volumes: | ||||
|       - /dev/shm:/dev/shm | ||||
|     network_mode: host | ||||
|  | ||||
| @ -1,13 +1,12 @@ | ||||
| """Test Enroll flow""" | ||||
| from time import sleep | ||||
| from sys import platform | ||||
| from typing import Any, Dict, Optional | ||||
| from unittest.case import skipUnless | ||||
|  | ||||
| from django.test import override_settings | ||||
| from docker import DockerClient, from_env | ||||
| from docker.models.containers import Container | ||||
| from docker.types import Healthcheck | ||||
| from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.support import expected_conditions as ec | ||||
| from structlog import get_logger | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||
| @ -18,41 +17,23 @@ from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage | ||||
| from passbook.stages.user_login.models import UserLoginStage | ||||
| from passbook.stages.user_write.models import UserWriteStage | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | ||||
| class TestFlowsEnroll(SeleniumTestCase): | ||||
|     """Test Enroll flow""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         self.container = self.setup_client() | ||||
|         super().setUp() | ||||
|  | ||||
|     def setup_client(self) -> Container: | ||||
|         """Setup test IdP container""" | ||||
|         client: DockerClient = from_env() | ||||
|         container = client.containers.run( | ||||
|             image="mailhog/mailhog:v1.0.1", | ||||
|             detach=True, | ||||
|             network_mode="host", | ||||
|             auto_remove=True, | ||||
|             healthcheck=Healthcheck( | ||||
|     def get_container_specs(self) -> Optional[Dict[str, Any]]: | ||||
|         return { | ||||
|             "image": "mailhog/mailhog:v1.0.1", | ||||
|             "detach": True, | ||||
|             "network_mode": "host", | ||||
|             "auto_remove": True, | ||||
|             "healthcheck": Healthcheck( | ||||
|                 test=["CMD", "wget", "--spider", "http://localhost:8025"], | ||||
|                 interval=5 * 100 * 1000000, | ||||
|                 start_period=1 * 100 * 1000000, | ||||
|             ), | ||||
|         ) | ||||
|         while True: | ||||
|             container.reload() | ||||
|             status = container.attrs.get("State", {}).get("Health", {}).get("Status") | ||||
|             if status == "healthy": | ||||
|                 return container | ||||
|             LOGGER.info("Container failed healthcheck") | ||||
|             sleep(1) | ||||
|  | ||||
|     def tearDown(self): | ||||
|         self.container.kill() | ||||
|         super().tearDown() | ||||
|         } | ||||
|  | ||||
|     def test_enroll_2_step(self): | ||||
|         """Test 2-step enroll flow""" | ||||
| @ -220,21 +201,25 @@ class TestFlowsEnroll(SeleniumTestCase): | ||||
|         self.driver.find_element(By.ID, "id_name").send_keys("some name") | ||||
|         self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz") | ||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click() | ||||
|         sleep(3) | ||||
|         # Wait for the success message so we know the email is sent | ||||
|         self.wait.until( | ||||
|             ec.presence_of_element_located((By.CSS_SELECTOR, ".pf-c-form > p")) | ||||
|         ) | ||||
|  | ||||
|         # Open Mailhog | ||||
|         self.driver.get("http://localhost:8025") | ||||
|  | ||||
|         # Click on first message | ||||
|         self.wait.until( | ||||
|             ec.presence_of_element_located((By.CLASS_NAME, "msglist-message")) | ||||
|         ) | ||||
|         self.driver.find_element(By.CLASS_NAME, "msglist-message").click() | ||||
|         sleep(3) | ||||
|         self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane")) | ||||
|         self.driver.find_element(By.ID, "confirm").click() | ||||
|         self.driver.close() | ||||
|         self.driver.switch_to.window(self.driver.window_handles[0]) | ||||
|  | ||||
|         # We're now logged in | ||||
|         sleep(3) | ||||
|         self.wait.until( | ||||
|             ec.presence_of_element_located( | ||||
|                 (By.XPATH, "//a[contains(@href, '/-/user/')]") | ||||
|  | ||||
| @ -1,10 +1,14 @@ | ||||
| """test default login flow""" | ||||
| from sys import platform | ||||
| from unittest.case import skipUnless | ||||
|  | ||||
| from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.common.keys import Keys | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
|  | ||||
|  | ||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | ||||
| class TestFlowsLogin(SeleniumTestCase): | ||||
|     """test default login flow""" | ||||
|  | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| """test stage setup flows (password change)""" | ||||
| import string | ||||
| from random import SystemRandom | ||||
| from time import sleep | ||||
| from sys import platform | ||||
| from unittest.case import skipUnless | ||||
|  | ||||
| from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.common.keys import Keys | ||||
| @ -9,9 +8,11 @@ from selenium.webdriver.common.keys import Keys | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| from passbook.core.models import User | ||||
| from passbook.flows.models import Flow, FlowDesignation | ||||
| from passbook.providers.oauth2.generators import generate_client_secret | ||||
| from passbook.stages.password.models import PasswordStage | ||||
|  | ||||
|  | ||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | ||||
| class TestFlowsStageSetup(SeleniumTestCase): | ||||
|     """test stage setup flows""" | ||||
|  | ||||
| @ -27,10 +28,7 @@ class TestFlowsStageSetup(SeleniumTestCase): | ||||
|         stage.change_flow = flow | ||||
|         stage.save() | ||||
|  | ||||
|         new_password = "".join( | ||||
|             SystemRandom().choice(string.ascii_uppercase + string.digits) | ||||
|             for _ in range(8) | ||||
|         ) | ||||
|         new_password = generate_client_secret() | ||||
|  | ||||
|         self.driver.get( | ||||
|             f"{self.live_server_url}/flows/default-authentication-flow/?next=%2F" | ||||
| @ -48,7 +46,7 @@ class TestFlowsStageSetup(SeleniumTestCase): | ||||
|         self.driver.find_element(By.ID, "id_password_repeat").send_keys(new_password) | ||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click() | ||||
|  | ||||
|         sleep(2) | ||||
|         self.wait_for_url(self.url("passbook_core:user-settings")) | ||||
|         # Because USER() is cached, we need to get the user manually here | ||||
|         user = User.objects.get(username=USER().username) | ||||
|         self.assertTrue(user.check_password(new_password)) | ||||
|  | ||||
| @ -1,12 +1,12 @@ | ||||
| """test OAuth Provider flow""" | ||||
| from time import sleep | ||||
| from sys import platform | ||||
| from typing import Any, Dict, Optional | ||||
| from unittest.case import skipUnless | ||||
|  | ||||
| from docker import DockerClient, from_env | ||||
| from docker.models.containers import Container | ||||
| from docker.types import Healthcheck | ||||
| from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.common.keys import Keys | ||||
| from structlog import get_logger | ||||
| from selenium.webdriver.support import expected_conditions as ec | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| from passbook.core.models import Application | ||||
| @ -19,32 +19,29 @@ from passbook.providers.oauth2.generators import ( | ||||
| ) | ||||
| from passbook.providers.oauth2.models import ClientTypes, OAuth2Provider, ResponseTypes | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | ||||
| class TestProviderOAuth2Github(SeleniumTestCase): | ||||
|     """test OAuth Provider flow""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         self.client_id = generate_client_id() | ||||
|         self.client_secret = generate_client_secret() | ||||
|         self.container = self.setup_client() | ||||
|         super().setUp() | ||||
|  | ||||
|     def setup_client(self) -> Container: | ||||
|     def get_container_specs(self) -> Optional[Dict[str, Any]]: | ||||
|         """Setup client grafana container which we test OAuth against""" | ||||
|         client: DockerClient = from_env() | ||||
|         container = client.containers.run( | ||||
|             image="grafana/grafana:7.1.0", | ||||
|             detach=True, | ||||
|             network_mode="host", | ||||
|             auto_remove=True, | ||||
|             healthcheck=Healthcheck( | ||||
|         return { | ||||
|             "image": "grafana/grafana:7.1.0", | ||||
|             "detach": True, | ||||
|             "network_mode": "host", | ||||
|             "auto_remove": True, | ||||
|             "healthcheck": Healthcheck( | ||||
|                 test=["CMD", "wget", "--spider", "http://localhost:3000"], | ||||
|                 interval=5 * 100 * 1000000, | ||||
|                 start_period=1 * 100 * 1000000, | ||||
|             ), | ||||
|             environment={ | ||||
|             "environment": { | ||||
|                 "GF_AUTH_GITHUB_ENABLED": "true", | ||||
|                 "GF_AUTH_GITHUB_ALLOW_SIGN_UP": "true", | ||||
|                 "GF_AUTH_GITHUB_CLIENT_ID": self.client_id, | ||||
| @ -61,22 +58,10 @@ class TestProviderOAuth2Github(SeleniumTestCase): | ||||
|                 ), | ||||
|                 "GF_LOG_LEVEL": "debug", | ||||
|             }, | ||||
|         ) | ||||
|         while True: | ||||
|             container.reload() | ||||
|             status = container.attrs.get("State", {}).get("Health", {}).get("Status") | ||||
|             if status == "healthy": | ||||
|                 return container | ||||
|             LOGGER.info("Container failed healthcheck") | ||||
|             sleep(1) | ||||
|  | ||||
|     def tearDown(self): | ||||
|         self.container.kill() | ||||
|         super().tearDown() | ||||
|         } | ||||
|  | ||||
|     def test_authorization_consent_implied(self): | ||||
|         """test OAuth Provider flow (default authorization flow with implied consent)""" | ||||
|         sleep(1) | ||||
|         # Bootstrap all needed objects | ||||
|         authorization_flow = Flow.objects.get( | ||||
|             slug="default-provider-authorization-implicit-consent" | ||||
| @ -129,7 +114,6 @@ class TestProviderOAuth2Github(SeleniumTestCase): | ||||
|  | ||||
|     def test_authorization_consent_explicit(self): | ||||
|         """test OAuth Provider flow (default authorization flow with explicit consent)""" | ||||
|         sleep(1) | ||||
|         # Bootstrap all needed objects | ||||
|         authorization_flow = Flow.objects.get( | ||||
|             slug="default-provider-authorization-explicit-consent" | ||||
| @ -167,8 +151,13 @@ class TestProviderOAuth2Github(SeleniumTestCase): | ||||
|                 By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]" | ||||
|             ).text, | ||||
|         ) | ||||
|         sleep(1) | ||||
|         self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click() | ||||
|         self.driver.find_element( | ||||
|             By.CSS_SELECTOR, | ||||
|             ( | ||||
|                 "form[action='/flows/b/default-provider-authorization-explicit-consent/'] " | ||||
|                 "[type=submit]" | ||||
|             ), | ||||
|         ).click() | ||||
|  | ||||
|         self.wait_for_url("http://localhost:3000/?orgId=1") | ||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click() | ||||
| @ -197,7 +186,6 @@ class TestProviderOAuth2Github(SeleniumTestCase): | ||||
|  | ||||
|     def test_denied(self): | ||||
|         """test OAuth Provider flow (default authorization flow, denied)""" | ||||
|         sleep(1) | ||||
|         # Bootstrap all needed objects | ||||
|         authorization_flow = Flow.objects.get( | ||||
|             slug="default-provider-authorization-explicit-consent" | ||||
| @ -227,7 +215,10 @@ class TestProviderOAuth2Github(SeleniumTestCase): | ||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||
|         self.wait_for_url(self.url("passbook_flows:denied")) | ||||
|  | ||||
|         self.wait.until( | ||||
|             ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1")) | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             self.driver.find_element(By.CSS_SELECTOR, "header > h1").text, | ||||
|             "Permission denied", | ||||
|  | ||||
| @ -1,8 +1,9 @@ | ||||
| """test OAuth2 OpenID Provider flow""" | ||||
| from sys import platform | ||||
| from time import sleep | ||||
| from typing import Any, Dict, Optional | ||||
| from unittest.case import skipUnless | ||||
|  | ||||
| from docker import DockerClient, from_env | ||||
| from docker.models.containers import Container | ||||
| from docker.types import Healthcheck | ||||
| from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.common.keys import Keys | ||||
| @ -34,29 +35,27 @@ from passbook.providers.oauth2.models import ( | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | ||||
| class TestProviderOAuth2OIDC(SeleniumTestCase): | ||||
|     """test OAuth with OpenID Provider flow""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         self.client_id = generate_client_id() | ||||
|         self.client_secret = generate_client_secret() | ||||
|         self.container = self.setup_client() | ||||
|         super().setUp() | ||||
|  | ||||
|     def setup_client(self) -> Container: | ||||
|         """Setup client grafana container which we test OIDC against""" | ||||
|         client: DockerClient = from_env() | ||||
|         container = client.containers.run( | ||||
|             image="grafana/grafana:7.1.0", | ||||
|             detach=True, | ||||
|             network_mode="host", | ||||
|             auto_remove=True, | ||||
|             healthcheck=Healthcheck( | ||||
|     def get_container_specs(self) -> Optional[Dict[str, Any]]: | ||||
|         return { | ||||
|             "image": "grafana/grafana:7.1.0", | ||||
|             "detach": True, | ||||
|             "network_mode": "host", | ||||
|             "auto_remove": True, | ||||
|             "healthcheck": Healthcheck( | ||||
|                 test=["CMD", "wget", "--spider", "http://localhost:3000"], | ||||
|                 interval=5 * 100 * 1000000, | ||||
|                 start_period=1 * 100 * 1000000, | ||||
|             ), | ||||
|             environment={ | ||||
|             "environment": { | ||||
|                 "GF_AUTH_GENERIC_OAUTH_ENABLED": "true", | ||||
|                 "GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id, | ||||
|                 "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret, | ||||
| @ -72,18 +71,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | ||||
|                 ), | ||||
|                 "GF_LOG_LEVEL": "debug", | ||||
|             }, | ||||
|         ) | ||||
|         while True: | ||||
|             container.reload() | ||||
|             status = container.attrs.get("State", {}).get("Health", {}).get("Status") | ||||
|             if status == "healthy": | ||||
|                 return container | ||||
|             LOGGER.info("Container failed healthcheck") | ||||
|             sleep(1) | ||||
|  | ||||
|     def tearDown(self): | ||||
|         self.container.kill() | ||||
|         super().tearDown() | ||||
|         } | ||||
|  | ||||
|     def test_redirect_uri_error(self): | ||||
|         """test OpenID Provider flow (invalid redirect URI, check error message)""" | ||||
| @ -297,7 +285,10 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | ||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||
|         self.wait_for_url(self.url("passbook_flows:denied")) | ||||
|  | ||||
|         self.wait.until( | ||||
|             ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1")) | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             self.driver.find_element(By.CSS_SELECTOR, "header > h1").text, | ||||
|             "Permission denied", | ||||
|  | ||||
							
								
								
									
										96
									
								
								e2e/test_provider_proxy.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								e2e/test_provider_proxy.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,96 @@ | ||||
| """Proxy and Outpost e2e tests""" | ||||
| from sys import platform | ||||
| from time import sleep | ||||
| from typing import Any, Dict, Optional | ||||
| from unittest.case import skipUnless | ||||
|  | ||||
| from docker.client import DockerClient, from_env | ||||
| from docker.models.containers import Container | ||||
| from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.common.keys import Keys | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| from passbook.core.models import Application | ||||
| from passbook.flows.models import Flow | ||||
| from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType | ||||
| from passbook.providers.proxy.models import ProxyProvider | ||||
|  | ||||
|  | ||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | ||||
| class TestProviderProxy(SeleniumTestCase): | ||||
|     """Proxy and Outpost e2e tests""" | ||||
|  | ||||
|     proxy_container: Container | ||||
|  | ||||
|     def tearDown(self) -> None: | ||||
|         super().tearDown() | ||||
|         self.proxy_container.kill() | ||||
|  | ||||
|     def get_container_specs(self) -> Optional[Dict[str, Any]]: | ||||
|         return { | ||||
|             "image": "traefik/whoami:latest", | ||||
|             "detach": True, | ||||
|             "network_mode": "host", | ||||
|             "auto_remove": True, | ||||
|         } | ||||
|  | ||||
|     def start_proxy(self, outpost: Outpost) -> Container: | ||||
|         """Start proxy container based on outpost created""" | ||||
|         client: DockerClient = from_env() | ||||
|         container = client.containers.run( | ||||
|             image="beryju/passbook-proxy:latest", | ||||
|             detach=True, | ||||
|             network_mode="host", | ||||
|             auto_remove=True, | ||||
|             environment={ | ||||
|                 "PASSBOOK_HOST": self.live_server_url, | ||||
|                 "PASSBOOK_TOKEN": outpost.token.token_uuid.hex, | ||||
|             }, | ||||
|         ) | ||||
|         return container | ||||
|  | ||||
|     def test_proxy_simple(self): | ||||
|         """Test simple outpost setup with single provider""" | ||||
|         proxy: ProxyProvider = ProxyProvider.objects.create( | ||||
|             name="proxy_provider", | ||||
|             authorization_flow=Flow.objects.get( | ||||
|                 slug="default-provider-authorization-implicit-consent" | ||||
|             ), | ||||
|             internal_host="http://localhost:80", | ||||
|             external_host="http://localhost:4180", | ||||
|         ) | ||||
|         # Ensure OAuth2 Params are set | ||||
|         proxy.set_oauth_defaults() | ||||
|         proxy.save() | ||||
|         # we need to create an application to actually access the proxy | ||||
|         Application.objects.create(name="proxy", slug="proxy", provider=proxy) | ||||
|         outpost: Outpost = Outpost.objects.create( | ||||
|             name="proxy_outpost", | ||||
|             type=OutpostType.PROXY, | ||||
|             deployment_type=OutpostDeploymentType.CUSTOM, | ||||
|         ) | ||||
|         outpost.providers.add(proxy) | ||||
|         outpost.save() | ||||
|  | ||||
|         self.proxy_container = self.start_proxy(outpost) | ||||
|  | ||||
|         # Wait until outpost healthcheck succeeds | ||||
|         healthcheck_retries = 0 | ||||
|         while healthcheck_retries < 50: | ||||
|             if outpost.health: | ||||
|                 break | ||||
|             healthcheck_retries += 1 | ||||
|             sleep(0.5) | ||||
|  | ||||
|         self.driver.get("http://localhost:4180") | ||||
|  | ||||
|         self.driver.find_element(By.ID, "id_uid_field").click() | ||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) | ||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||
|  | ||||
|         sleep(1) | ||||
|  | ||||
|         full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text | ||||
|         self.assertIn("X-Forwarded-Preferred-Username: pbadmin", full_body_text) | ||||
| @ -1,11 +1,14 @@ | ||||
| """test SAML Provider flow""" | ||||
| from sys import platform | ||||
| from time import sleep | ||||
| from unittest.case import skipUnless | ||||
|  | ||||
| from docker import DockerClient, from_env | ||||
| from docker.models.containers import Container | ||||
| from docker.types import Healthcheck | ||||
| from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.common.keys import Keys | ||||
| from selenium.webdriver.support import expected_conditions as ec | ||||
| from structlog import get_logger | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| @ -23,6 +26,7 @@ from passbook.providers.saml.models import ( | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | ||||
| class TestProviderSAML(SeleniumTestCase): | ||||
|     """test SAML Provider flow""" | ||||
|  | ||||
| @ -60,10 +64,6 @@ class TestProviderSAML(SeleniumTestCase): | ||||
|             LOGGER.info("Container failed healthcheck") | ||||
|             sleep(1) | ||||
|  | ||||
|     def tearDown(self): | ||||
|         self.container.kill() | ||||
|         super().tearDown() | ||||
|  | ||||
|     def test_sp_initiated_implicit(self): | ||||
|         """test SAML Provider flow SP-initiated flow (implicit consent)""" | ||||
|         # Bootstrap all needed objects | ||||
| @ -207,7 +207,10 @@ class TestProviderSAML(SeleniumTestCase): | ||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||
|         self.wait_for_url(self.url("passbook_flows:denied")) | ||||
|  | ||||
|         self.wait.until( | ||||
|             ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1")) | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             self.driver.find_element(By.CSS_SELECTOR, "header > h1").text, | ||||
|             "Permission denied", | ||||
|  | ||||
| @ -1,8 +1,11 @@ | ||||
| """test OAuth Source""" | ||||
| from os.path import abspath | ||||
| from sys import platform | ||||
| from time import sleep | ||||
| from typing import Any, Dict, Optional | ||||
| from unittest.case import skipUnless | ||||
| 
 | ||||
| from docker import DockerClient, from_env | ||||
| from django.test import override_settings | ||||
| from docker.models.containers import Container | ||||
| from docker.types import Healthcheck | ||||
| from selenium.webdriver.common.by import By | ||||
| @ -21,6 +24,7 @@ CONFIG_PATH = "/tmp/dex.yml" | ||||
| LOGGER = get_logger() | ||||
| 
 | ||||
| 
 | ||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | ||||
| class TestSourceOAuth(SeleniumTestCase): | ||||
|     """test OAuth Source flow""" | ||||
| 
 | ||||
| @ -28,7 +32,7 @@ class TestSourceOAuth(SeleniumTestCase): | ||||
| 
 | ||||
|     def setUp(self): | ||||
|         self.client_secret = generate_client_secret() | ||||
|         self.container = self.setup_client() | ||||
|         self.prepare_dex_config() | ||||
|         super().setUp() | ||||
| 
 | ||||
|     def prepare_dex_config(self): | ||||
| @ -66,34 +70,23 @@ class TestSourceOAuth(SeleniumTestCase): | ||||
|         with open(CONFIG_PATH, "w+") as _file: | ||||
|             safe_dump(config, _file) | ||||
| 
 | ||||
|     def setup_client(self) -> Container: | ||||
|         """Setup test Dex container""" | ||||
|         self.prepare_dex_config() | ||||
|         client: DockerClient = from_env() | ||||
|         container = client.containers.run( | ||||
|             image="quay.io/dexidp/dex:v2.24.0", | ||||
|             detach=True, | ||||
|             network_mode="host", | ||||
|             auto_remove=True, | ||||
|             command="serve /config.yml", | ||||
|             healthcheck=Healthcheck( | ||||
|     def get_container_specs(self) -> Optional[Dict[str, Any]]: | ||||
|         return { | ||||
|             "image": "quay.io/dexidp/dex:v2.24.0", | ||||
|             "detach": True, | ||||
|             "network_mode": "host", | ||||
|             "auto_remove": True, | ||||
|             "command": "serve /config.yml", | ||||
|             "healthcheck": Healthcheck( | ||||
|                 test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"], | ||||
|                 interval=5 * 100 * 1000000, | ||||
|                 start_period=1 * 100 * 1000000, | ||||
|             ), | ||||
|             volumes={abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro"}}, | ||||
|         ) | ||||
|         while True: | ||||
|             container.reload() | ||||
|             status = container.attrs.get("State", {}).get("Health", {}).get("Status") | ||||
|             if status == "healthy": | ||||
|                 return container | ||||
|             LOGGER.info("Container failed healthcheck") | ||||
|             sleep(1) | ||||
|             "volumes": {abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro"}}, | ||||
|         } | ||||
| 
 | ||||
|     def create_objects(self): | ||||
|         """Create required objects""" | ||||
|         sleep(1) | ||||
|         # Bootstrap all needed objects | ||||
|         authentication_flow = Flow.objects.get(slug="default-source-authentication") | ||||
|         enrollment_flow = Flow.objects.get(slug="default-source-enrollment") | ||||
| @ -111,10 +104,6 @@ class TestSourceOAuth(SeleniumTestCase): | ||||
|             consumer_secret=self.client_secret, | ||||
|         ) | ||||
| 
 | ||||
|     def tearDown(self): | ||||
|         self.container.kill() | ||||
|         super().tearDown() | ||||
| 
 | ||||
|     def test_oauth_enroll(self): | ||||
|         """test OAuth Source With With OIDC""" | ||||
|         self.create_objects() | ||||
| @ -141,6 +130,7 @@ class TestSourceOAuth(SeleniumTestCase): | ||||
|         ) | ||||
|         self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click() | ||||
| 
 | ||||
|         self.wait.until(ec.presence_of_element_located((By.NAME, "username"))) | ||||
|         # At this point we've been redirected back | ||||
|         # and we're asked for the username | ||||
|         self.driver.find_element(By.NAME, "username").click() | ||||
| @ -167,6 +157,42 @@ class TestSourceOAuth(SeleniumTestCase): | ||||
|             "admin@example.com", | ||||
|         ) | ||||
| 
 | ||||
|     @override_settings(SESSION_COOKIE_SAMESITE="strict") | ||||
|     def test_oauth_samesite_strict(self): | ||||
|         """test OAuth Source With SameSite set to strict | ||||
|         (=will fail because session is not carried over)""" | ||||
|         self.create_objects() | ||||
|         self.driver.get(self.live_server_url) | ||||
| 
 | ||||
|         self.wait.until( | ||||
|             ec.presence_of_element_located( | ||||
|                 (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") | ||||
|             ) | ||||
|         ) | ||||
|         self.driver.find_element( | ||||
|             By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" | ||||
|         ).click() | ||||
| 
 | ||||
|         # Now we should be at the IDP, wait for the login field | ||||
|         self.wait.until(ec.presence_of_element_located((By.ID, "login"))) | ||||
|         self.driver.find_element(By.ID, "login").send_keys("admin@example.com") | ||||
|         self.driver.find_element(By.ID, "password").send_keys("password") | ||||
|         self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER) | ||||
| 
 | ||||
|         # Wait until we're logged in | ||||
|         self.wait.until( | ||||
|             ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]")) | ||||
|         ) | ||||
|         self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click() | ||||
| 
 | ||||
|         self.wait.until( | ||||
|             ec.presence_of_element_located((By.CSS_SELECTOR, ".pf-c-alert__title")) | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             self.driver.find_element(By.CSS_SELECTOR, ".pf-c-alert__title").text, | ||||
|             "Authentication Failed.", | ||||
|         ) | ||||
| 
 | ||||
|     def test_oauth_enroll_auth(self): | ||||
|         """test OAuth Source With With OIDC (enroll and authenticate again)""" | ||||
|         self.test_oauth_enroll() | ||||
| @ -178,10 +204,11 @@ class TestSourceOAuth(SeleniumTestCase): | ||||
|                 (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") | ||||
|             ) | ||||
|         ) | ||||
|         sleep(1) | ||||
|         self.driver.find_element( | ||||
|             By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" | ||||
|         ).click() | ||||
| 
 | ||||
|         sleep(1) | ||||
|         # Now we should be at the IDP, wait for the login field | ||||
|         self.wait.until(ec.presence_of_element_located((By.ID, "login"))) | ||||
|         self.driver.find_element(By.ID, "login").send_keys("admin@example.com") | ||||
| @ -1,8 +1,9 @@ | ||||
| """test SAML Source""" | ||||
| from sys import platform | ||||
| from time import sleep | ||||
| from typing import Any, Dict, Optional | ||||
| from unittest.case import skipUnless | ||||
|  | ||||
| from docker import DockerClient, from_env | ||||
| from docker.models.containers import Container | ||||
| from docker.types import Healthcheck | ||||
| from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.common.keys import Keys | ||||
| @ -68,48 +69,31 @@ Sm75WXsflOxuTn08LbgGc4s= | ||||
| -----END PRIVATE KEY-----""" | ||||
|  | ||||
|  | ||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | ||||
| class TestSourceSAML(SeleniumTestCase): | ||||
|     """test SAML Source flow""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         self.container = self.setup_client() | ||||
|         super().setUp() | ||||
|  | ||||
|     def setup_client(self) -> Container: | ||||
|         """Setup test IdP container""" | ||||
|         client: DockerClient = from_env() | ||||
|         container = client.containers.run( | ||||
|             image="kristophjunge/test-saml-idp:1.15", | ||||
|             detach=True, | ||||
|             network_mode="host", | ||||
|             auto_remove=True, | ||||
|             healthcheck=Healthcheck( | ||||
|     def get_container_specs(self) -> Optional[Dict[str, Any]]: | ||||
|         return { | ||||
|             "image": "kristophjunge/test-saml-idp:1.15", | ||||
|             "detach": True, | ||||
|             "network_mode": "host", | ||||
|             "auto_remove": True, | ||||
|             "healthcheck": Healthcheck( | ||||
|                 test=["CMD", "curl", "http://localhost:8080"], | ||||
|                 interval=5 * 100 * 1000000, | ||||
|                 start_period=1 * 100 * 1000000, | ||||
|             ), | ||||
|             environment={ | ||||
|             "environment": { | ||||
|                 "SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id", | ||||
|                 "SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE": ( | ||||
|                     f"{self.live_server_url}/source/saml/saml-idp-test/acs/" | ||||
|                 ), | ||||
|             }, | ||||
|         ) | ||||
|         while True: | ||||
|             container.reload() | ||||
|             status = container.attrs.get("State", {}).get("Health", {}).get("Status") | ||||
|             if status == "healthy": | ||||
|                 return container | ||||
|             LOGGER.info("Container failed healthcheck") | ||||
|             sleep(1) | ||||
|  | ||||
|     def tearDown(self): | ||||
|         self.container.kill() | ||||
|         super().tearDown() | ||||
|         } | ||||
|  | ||||
|     def test_idp_redirect(self): | ||||
|         """test SAML Source With redirect binding""" | ||||
|         sleep(1) | ||||
|         # Bootstrap all needed objects | ||||
|         authentication_flow = Flow.objects.get(slug="default-source-authentication") | ||||
|         enrollment_flow = Flow.objects.get(slug="default-source-enrollment") | ||||
| @ -161,7 +145,6 @@ class TestSourceSAML(SeleniumTestCase): | ||||
|  | ||||
|     def test_idp_post(self): | ||||
|         """test SAML Source With post binding""" | ||||
|         sleep(1) | ||||
|         # Bootstrap all needed objects | ||||
|         authentication_flow = Flow.objects.get(slug="default-source-authentication") | ||||
|         enrollment_flow = Flow.objects.get(slug="default-source-enrollment") | ||||
| @ -215,7 +198,6 @@ class TestSourceSAML(SeleniumTestCase): | ||||
|  | ||||
|     def test_idp_post_auto(self): | ||||
|         """test SAML Source With post binding (auto redirect)""" | ||||
|         sleep(1) | ||||
|         # Bootstrap all needed objects | ||||
|         authentication_flow = Flow.objects.get(slug="default-source-authentication") | ||||
|         enrollment_flow = Flow.objects.get(slug="default-source-enrollment") | ||||
|  | ||||
							
								
								
									
										33
									
								
								e2e/utils.py
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								e2e/utils.py
									
									
									
									
									
								
							| @ -4,13 +4,16 @@ from glob import glob | ||||
| from importlib.util import module_from_spec, spec_from_file_location | ||||
| from inspect import getmembers, isfunction | ||||
| from os import environ, makedirs | ||||
| from time import time | ||||
| from time import sleep, time | ||||
| from typing import Any, Dict, Optional | ||||
|  | ||||
| from django.apps import apps | ||||
| from django.contrib.staticfiles.testing import StaticLiveServerTestCase | ||||
| from django.db import connection, transaction | ||||
| from django.db.utils import IntegrityError | ||||
| from django.shortcuts import reverse | ||||
| from docker import DockerClient, from_env | ||||
| from docker.models.containers import Container | ||||
| from selenium import webdriver | ||||
| from selenium.webdriver.common.desired_capabilities import DesiredCapabilities | ||||
| from selenium.webdriver.remote.webdriver import WebDriver | ||||
| @ -30,15 +33,37 @@ def USER() -> User:  # noqa | ||||
| class SeleniumTestCase(StaticLiveServerTestCase): | ||||
|     """StaticLiveServerTestCase which automatically creates a Webdriver instance""" | ||||
|  | ||||
|     container: Optional[Container] = None | ||||
|  | ||||
|     def setUp(self): | ||||
|         super().setUp() | ||||
|         makedirs("selenium_screenshots/", exist_ok=True) | ||||
|         self.driver = self._get_driver() | ||||
|         self.driver.maximize_window() | ||||
|         self.driver.implicitly_wait(30) | ||||
|         self.wait = WebDriverWait(self.driver, 50) | ||||
|         self.driver.implicitly_wait(10) | ||||
|         self.wait = WebDriverWait(self.driver, 30) | ||||
|         self.apply_default_data() | ||||
|         self.logger = get_logger() | ||||
|         if specs := self.get_container_specs(): | ||||
|             self.container = self._start_container(specs) | ||||
|  | ||||
|     def _start_container(self, specs: Dict[str, Any]) -> Container: | ||||
|         client: DockerClient = from_env() | ||||
|         container = client.containers.run(**specs) | ||||
|         if "healthcheck" not in specs: | ||||
|             return container | ||||
|         while True: | ||||
|             container.reload() | ||||
|             status = container.attrs.get("State", {}).get("Health", {}).get("Status") | ||||
|             if status == "healthy": | ||||
|                 return container | ||||
|             self.logger.info("Container failed healthcheck") | ||||
|             sleep(1) | ||||
|  | ||||
|     def get_container_specs(self) -> Optional[Dict[str, Any]]: | ||||
|         """Optionally get container specs which will launched on setup, wait for the container to | ||||
|         be healthy, and deleted again on tearDown""" | ||||
|         return None | ||||
|  | ||||
|     def _get_driver(self) -> WebDriver: | ||||
|         return webdriver.Remote( | ||||
| @ -57,6 +82,8 @@ class SeleniumTestCase(StaticLiveServerTestCase): | ||||
|             self.logger.warning( | ||||
|                 line["message"], source=line["source"], level=line["level"] | ||||
|             ) | ||||
|         if self.container: | ||||
|             self.container.kill() | ||||
|         self.driver.quit() | ||||
|         super().tearDown() | ||||
|  | ||||
|  | ||||
| @ -1,15 +1,15 @@ | ||||
| apiVersion: v2 | ||||
| appVersion: "0.10.0-rc1" | ||||
| appVersion: "0.10.3-stable" | ||||
| description: A Helm chart for passbook. | ||||
| name: passbook | ||||
| version: "0.10.0-rc1" | ||||
| icon: https://github.com/BeryJu/passbook/blob/master/passbook/static/static/passbook/logo.svg | ||||
| version: "0.10.3-stable" | ||||
| icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg | ||||
| dependencies: | ||||
|   - name: postgresql | ||||
|     version: 9.3.2 | ||||
|     version: 9.4.1 | ||||
|     repository: https://charts.bitnami.com/bitnami | ||||
|     condition: install.postgresql | ||||
|   - name: redis | ||||
|     version: 10.7.16 | ||||
|     version: 10.9.0 | ||||
|     repository: https://charts.bitnami.com/bitnami | ||||
|     condition: install.redis | ||||
|  | ||||
| @ -9,7 +9,7 @@ metadata: | ||||
|     app.kubernetes.io/managed-by: {{ .Release.Service }} | ||||
|     k8s.passbook.beryju.org/component: web | ||||
| spec: | ||||
|   replicas: {{ serverReplicas }} | ||||
|   replicas: {{ .Values.serverReplicas }} | ||||
|   selector: | ||||
|     matchLabels: | ||||
|       app.kubernetes.io/name: {{ include "passbook.name" . }} | ||||
| @ -22,10 +22,29 @@ spec: | ||||
|         app.kubernetes.io/instance: {{ .Release.Name }} | ||||
|         k8s.passbook.beryju.org/component: web | ||||
|     spec: | ||||
|       affinity: | ||||
|         podAntiAffinity: | ||||
|           preferredDuringSchedulingIgnoredDuringExecution: | ||||
|           - weight: 1 | ||||
|             podAffinityTerm: | ||||
|               labelSelector: | ||||
|                 matchExpressions: | ||||
|                 - key: app.kubernetes.io/name | ||||
|                   operator: In | ||||
|                   values: | ||||
|                   - {{ include "passbook.name" . }} | ||||
|                 - key: app.kubernetes.io/instance | ||||
|                   operator: In | ||||
|                   values: | ||||
|                   - {{ .Release.Name }} | ||||
|                 - key: k8s.passbook.beryju.org/component | ||||
|                   operator: In | ||||
|                   values: | ||||
|                   - web | ||||
|               topologyKey: "kubernetes.io/hostname" | ||||
|       initContainers: | ||||
|         - name: passbook-database-migrations | ||||
|           image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" | ||||
|           imagePullPolicy: Always | ||||
|           args: [migrate] | ||||
|           envFrom: | ||||
|             - configMapRef: | ||||
| @ -50,7 +69,6 @@ spec: | ||||
|       containers: | ||||
|         - name: {{ .Chart.Name }} | ||||
|           image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" | ||||
|           imagePullPolicy: Always | ||||
|           args: [server] | ||||
|           envFrom: | ||||
|             - configMapRef: | ||||
| @ -93,7 +111,7 @@ spec: | ||||
|           resources: | ||||
|             requests: | ||||
|               cpu: 100m | ||||
|               memory: 200M | ||||
|               memory: 300M | ||||
|             limits: | ||||
|               cpu: 300m | ||||
|               memory: 350M | ||||
|               memory: 500M | ||||
|  | ||||
| @ -9,7 +9,7 @@ metadata: | ||||
|     app.kubernetes.io/managed-by: {{ .Release.Service }} | ||||
|     k8s.passbook.beryju.org/component: worker | ||||
| spec: | ||||
|   replicas: {{ workerReplicas }} | ||||
|   replicas: {{ .Values.workerReplicas }} | ||||
|   selector: | ||||
|     matchLabels: | ||||
|       app.kubernetes.io/name: {{ include "passbook.name" . }} | ||||
| @ -22,6 +22,26 @@ spec: | ||||
|         app.kubernetes.io/instance: {{ .Release.Name }} | ||||
|         k8s.passbook.beryju.org/component: worker | ||||
|     spec: | ||||
|       affinity: | ||||
|         podAntiAffinity: | ||||
|           preferredDuringSchedulingIgnoredDuringExecution: | ||||
|           - weight: 1 | ||||
|             podAffinityTerm: | ||||
|               labelSelector: | ||||
|                 matchExpressions: | ||||
|                 - key: app.kubernetes.io/name | ||||
|                   operator: In | ||||
|                   values: | ||||
|                   - {{ include "passbook.name" . }} | ||||
|                 - key: app.kubernetes.io/instance | ||||
|                   operator: In | ||||
|                   values: | ||||
|                   - {{ .Release.Name }} | ||||
|                 - key: k8s.passbook.beryju.org/component | ||||
|                   operator: In | ||||
|                   values: | ||||
|                   - worker | ||||
|               topologyKey: "kubernetes.io/hostname" | ||||
|       containers: | ||||
|         - name: {{ .Chart.Name }} | ||||
|           image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" | ||||
| @ -50,7 +70,7 @@ spec: | ||||
|           resources: | ||||
|             requests: | ||||
|               cpu: 150m | ||||
|               memory: 300M | ||||
|               memory: 400M | ||||
|             limits: | ||||
|               cpu: 300m | ||||
|               memory: 500M | ||||
|               memory: 600M | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| image: | ||||
|   name: beryju/passbook | ||||
|   name_static: beryju/passbook-static | ||||
|   tag: 0.10.0-rc1 | ||||
|   tag: 0.10.3-stable | ||||
|  | ||||
| nameOverride: "" | ||||
|  | ||||
|  | ||||
| @ -4,7 +4,7 @@ printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", | ||||
| if [[ "$1" == "server" ]]; then | ||||
|     gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application | ||||
| elif [[ "$1" == "worker" ]]; then | ||||
|     celery worker --autoscale=10,3 -E -B -A=passbook.root.celery -s=/tmp/celerybeat-schedule | ||||
|     celery worker --autoscale=10,3 -E -B -A=passbook.root.celery -s=/tmp/celerybeat-schedule -Q passbook,passbook_scheduled | ||||
| elif [[ "$1" == "migrate" ]]; then | ||||
|     # Run system migrations first, run normal migrations after | ||||
|     python -m lifecycle.migrate | ||||
|  | ||||
| @ -1,9 +1,10 @@ | ||||
| """Gunicorn config""" | ||||
| from multiprocessing import cpu_count | ||||
| from pathlib import Path | ||||
|  | ||||
| import structlog | ||||
|  | ||||
| bind = "0.0.0.0:8000" | ||||
| workers = 2 | ||||
| threads = 4 | ||||
|  | ||||
| user = "passbook" | ||||
| group = "passbook" | ||||
| @ -40,3 +41,11 @@ logconfig_dict = { | ||||
|         "gunicorn": {"handlers": ["console"], "level": "INFO", "propagate": False}, | ||||
|     }, | ||||
| } | ||||
|  | ||||
| # if we're running in kubernetes, use fixed workers because we can scale with more pods | ||||
| # otherwise (assume docker-compose), use as much as we can | ||||
| if Path("/var/run/secrets/kubernetes.io").exists(): | ||||
|     workers = 2 | ||||
| else: | ||||
|     worker = cpu_count() * 2 + 1 | ||||
| threads = 4 | ||||
|  | ||||
| @ -30,7 +30,10 @@ nav: | ||||
|     - OAuth2: providers/oauth2.md | ||||
|     - SAML: providers/saml.md | ||||
|     - Proxy: providers/proxy.md | ||||
|   - Outposts: outposts/outposts.md | ||||
|   - Outposts: | ||||
|     - Overview: outposts/outposts.md | ||||
|     - Deploy on docker-compose: outposts/deploy-docker-compose.md | ||||
|     - Deploy on Kubernetes: outposts/deploy-kubernetes.md | ||||
|   - Expressions: | ||||
|     - Overview: expressions/index.md | ||||
|     - Reference: | ||||
| @ -49,9 +52,12 @@ nav: | ||||
|         - Harbor: integrations/services/harbor/index.md | ||||
|         - Sentry: integrations/services/sentry/index.md | ||||
|         - Ansible Tower/AWX: integrations/services/tower-awx/index.md | ||||
|         - VMware vCenter: integrations/services/vmware-vcenter/index.md | ||||
|   - Upgrading: | ||||
|     - to 0.9: upgrading/to-0.9.md | ||||
|     - to 0.10: upgrading/to-0.10.md | ||||
|   - Troubleshooting: | ||||
|     - Access problems: troubleshooting/access.md | ||||
|  | ||||
| repo_name: "BeryJu/passbook" | ||||
| repo_url: https://github.com/BeryJu/passbook | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook""" | ||||
| __version__ = "0.10.0-rc1" | ||||
| __version__ = "0.10.3-stable" | ||||
|  | ||||
| @ -15,11 +15,8 @@ class CodeMirrorWidget(forms.Textarea): | ||||
|         self.mode = mode | ||||
|  | ||||
|     def render(self, *args, **kwargs): | ||||
|         if "attrs" not in kwargs: | ||||
|             kwargs["attrs"] = {} | ||||
|         attrs = kwargs["attrs"] | ||||
|         if "class" not in attrs: | ||||
|             attrs["class"] = "" | ||||
|         attrs = kwargs.setdefault("attrs", {}) | ||||
|         attrs.setdefault("class", "") | ||||
|         attrs["class"] += " codemirror" | ||||
|         attrs["data-cm-mode"] = self.mode | ||||
|         return super().render(*args, **kwargs) | ||||
|  | ||||
| @ -12,7 +12,7 @@ class UserForm(forms.ModelForm): | ||||
|     class Meta: | ||||
|  | ||||
|         model = User | ||||
|         fields = ["username", "name", "email", "is_staff", "is_active", "attributes"] | ||||
|         fields = ["username", "name", "email", "is_active", "attributes"] | ||||
|         widgets = { | ||||
|             "name": forms.TextInput, | ||||
|             "attributes": CodeMirrorWidget, | ||||
|  | ||||
| @ -50,7 +50,7 @@ | ||||
|                     </td> | ||||
|                     <td role="cell"> | ||||
|                         <span> | ||||
|                             {{ group.user_set.all|length }} | ||||
|                             {{ group.users.all|length }} | ||||
|                         </span> | ||||
|                     </td> | ||||
|                     <td> | ||||
|  | ||||
| @ -69,6 +69,7 @@ | ||||
|                     <td> | ||||
|                         <a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:outpost-update' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> | ||||
|                         <a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:outpost-delete' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> | ||||
|                         <a href="https://passbook.beryju.org/outposts/outposts/#deploy">{% trans 'Deploy' %}</a> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|                 {% endfor %} | ||||
|  | ||||
| @ -55,7 +55,7 @@ | ||||
|                     <th role="columnheader"> | ||||
|                         <div> | ||||
|                             <div>{{ policy.name }}</div> | ||||
|                             {% if not policy.bindings.exists %} | ||||
|                             {% if not policy.bindings.exists and not policy.promptstage_set.exists %} | ||||
|                             <i class="pf-icon pf-icon-warning-triangle"></i> | ||||
|                             <small>{% trans 'Warning: Policy is not assigned.' %}</small> | ||||
|                             {% else %} | ||||
|  | ||||
| @ -8,7 +8,7 @@ from django.test import Client, TestCase | ||||
| from django.urls.exceptions import NoReverseMatch | ||||
|  | ||||
| from passbook.admin.urls import urlpatterns | ||||
| from passbook.core.models import User | ||||
| from passbook.core.models import Group, User | ||||
| from passbook.lib.utils.reflection import get_apps | ||||
|  | ||||
|  | ||||
| @ -16,7 +16,9 @@ class TestAdmin(TestCase): | ||||
|     """Generic admin tests""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         self.user = User.objects.create_superuser(username="test") | ||||
|         self.user = User.objects.create_user(username="test") | ||||
|         self.user.pb_groups.add(Group.objects.filter(is_superuser=True).first()) | ||||
|         self.user.save() | ||||
|         self.client = Client() | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
|  | ||||
| @ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( | ||||
| ) | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import ListView, UpdateView | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
|  | ||||
| @ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( | ||||
| ) | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import ListView, UpdateView | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
|  | ||||
| @ -7,7 +7,7 @@ from django.contrib.auth.mixins import ( | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.http import HttpRequest, HttpResponse, JsonResponse | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import DetailView, FormView, ListView, UpdateView | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
|  | ||||
| @ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( | ||||
| ) | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import ListView, UpdateView | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
|  | ||||
| @ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( | ||||
| ) | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import ListView, UpdateView | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
|  | ||||
| @ -1,8 +1,10 @@ | ||||
| """passbook administration overview""" | ||||
| from typing import Union | ||||
|  | ||||
| from django.core.cache import cache | ||||
| from django.shortcuts import redirect, reverse | ||||
| from django.views.generic import TemplateView | ||||
| from packaging.version import Version, parse | ||||
| from packaging.version import LegacyVersion, Version, parse | ||||
| from requests import RequestException, get | ||||
|  | ||||
| from passbook import __version__ | ||||
| @ -16,7 +18,7 @@ from passbook.stages.invitation.models import Invitation | ||||
| VERSION_CACHE_KEY = "passbook_latest_version" | ||||
|  | ||||
|  | ||||
| def latest_version() -> Version: | ||||
| def latest_version() -> Union[LegacyVersion, Version]: | ||||
|     """Get latest release from GitHub, cached""" | ||||
|     if not cache.get(VERSION_CACHE_KEY): | ||||
|         try: | ||||
| @ -45,7 +47,7 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView): | ||||
|     def get_context_data(self, **kwargs): | ||||
|         kwargs["application_count"] = len(Application.objects.all()) | ||||
|         kwargs["policy_count"] = len(Policy.objects.all()) | ||||
|         kwargs["user_count"] = len(User.objects.all()) | ||||
|         kwargs["user_count"] = len(User.objects.all()) - 1  # Remove anonymous user | ||||
|         kwargs["provider_count"] = len(Provider.objects.all()) | ||||
|         kwargs["source_count"] = len(Source.objects.all()) | ||||
|         kwargs["stage_count"] = len(Stage.objects.all()) | ||||
| @ -58,7 +60,7 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView): | ||||
|             application=None | ||||
|         ) | ||||
|         kwargs["policies_without_binding"] = len( | ||||
|             Policy.objects.filter(bindings__isnull=True) | ||||
|             Policy.objects.filter(bindings__isnull=True, promptstage__isnull=True) | ||||
|         ) | ||||
|         kwargs["cached_policies"] = len(cache.keys("policy_*")) | ||||
|         kwargs["cached_flows"] = len(cache.keys("flow_*")) | ||||
|  | ||||
| @ -10,7 +10,7 @@ from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.db.models import QuerySet | ||||
| from django.http import HttpResponse | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import FormView | ||||
| from django.views.generic.detail import DetailView | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
| @ -6,7 +6,7 @@ from django.contrib.auth.mixins import ( | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.db.models import QuerySet | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import ListView, UpdateView | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
|  | ||||
| @ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( | ||||
| ) | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
| from passbook.admin.views.utils import ( | ||||
|  | ||||
| @ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( | ||||
| ) | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
| from passbook.admin.views.utils import ( | ||||
|  | ||||
| @ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( | ||||
| ) | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
| from passbook.admin.views.utils import ( | ||||
|  | ||||
| @ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( | ||||
| ) | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
| from passbook.admin.views.utils import ( | ||||
|  | ||||
| @ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( | ||||
| ) | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import ListView, UpdateView | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
|  | ||||
| @ -6,7 +6,7 @@ from django.contrib.auth.mixins import ( | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.http import HttpResponseRedirect | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import ListView | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
|  | ||||
| @ -5,7 +5,7 @@ from django.contrib.auth.mixins import ( | ||||
| ) | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import ListView, UpdateView | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| """passbook Token administration""" | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import ListView | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
|  | ||||
| @ -9,7 +9,7 @@ from django.http import HttpRequest, HttpResponse | ||||
| from django.shortcuts import redirect | ||||
| from django.urls import reverse, reverse_lazy | ||||
| from django.utils.http import urlencode | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import DetailView, ListView, UpdateView | ||||
| from guardian.mixins import ( | ||||
|     PermissionListMixin, | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| """api v2 urls""" | ||||
| from django.conf.urls import url | ||||
| from django.urls import path | ||||
| from django.urls import path, re_path | ||||
| from drf_yasg import openapi | ||||
| from drf_yasg.views import get_schema_view | ||||
| from rest_framework import routers | ||||
| @ -119,7 +118,7 @@ SchemaView = get_schema_view( | ||||
| ) | ||||
|  | ||||
| urlpatterns = [ | ||||
|     url( | ||||
|     re_path( | ||||
|         r"^swagger(?P<format>\.json|\.yaml)$", | ||||
|         SchemaView.without_ui(cache_timeout=0), | ||||
|         name="schema-json", | ||||
|  | ||||
| @ -11,7 +11,7 @@ class GroupSerializer(ModelSerializer): | ||||
|     class Meta: | ||||
|  | ||||
|         model = Group | ||||
|         fields = ["pk", "name", "parent", "user_set", "attributes"] | ||||
|         fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"] | ||||
|  | ||||
|  | ||||
| class GroupViewSet(ModelViewSet): | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| """User API Views""" | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.serializers import BooleanField, ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from passbook.core.models import User | ||||
| @ -8,10 +8,12 @@ from passbook.core.models import User | ||||
| class UserSerializer(ModelSerializer): | ||||
|     """User Serializer""" | ||||
|  | ||||
|     is_superuser = BooleanField(read_only=True) | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = User | ||||
|         fields = ["pk", "username", "name", "email"] | ||||
|         fields = ["pk", "username", "name", "is_superuser", "email"] | ||||
|  | ||||
|  | ||||
| class UserViewSet(ModelViewSet): | ||||
|  | ||||
| @ -29,7 +29,16 @@ class ApplicationForm(forms.ModelForm): | ||||
|         ] | ||||
|         widgets = { | ||||
|             "name": forms.TextInput(), | ||||
|             "meta_launch_url": forms.TextInput(), | ||||
|             "meta_launch_url": forms.TextInput( | ||||
|                 attrs={ | ||||
|                     "placeholder": _( | ||||
|                         ( | ||||
|                             "If left empty, passbook will try to extract the launch URL " | ||||
|                             "based on the selected provider." | ||||
|                         ) | ||||
|                     ) | ||||
|                 } | ||||
|             ), | ||||
|             "meta_icon_url": forms.TextInput(), | ||||
|             "meta_publisher": forms.TextInput(), | ||||
|         } | ||||
|  | ||||
| @ -18,21 +18,19 @@ class GroupForm(forms.ModelForm): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         if self.instance.pk: | ||||
|             self.initial["members"] = self.instance.user_set.values_list( | ||||
|                 "pk", flat=True | ||||
|             ) | ||||
|             self.initial["members"] = self.instance.users.values_list("pk", flat=True) | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         instance = super().save(*args, **kwargs) | ||||
|         if instance.pk: | ||||
|             instance.user_set.clear() | ||||
|             instance.user_set.add(*self.cleaned_data["members"]) | ||||
|             instance.users.clear() | ||||
|             instance.users.add(*self.cleaned_data["members"]) | ||||
|         return instance | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = Group | ||||
|         fields = ["name", "parent", "members", "attributes"] | ||||
|         fields = ["name", "is_superuser", "parent", "members", "attributes"] | ||||
|         widgets = { | ||||
|             "name": forms.TextInput(), | ||||
|             "attributes": CodeMirrorWidget, | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| # Generated by Django 3.0.6 on 2020-05-23 16:40 | ||||
|  | ||||
| from django.apps.registry import Apps | ||||
| from django.db import migrations | ||||
| from django.db import migrations, models | ||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||
|  | ||||
|  | ||||
| @ -15,8 +15,6 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|         username="pbadmin", email="root@localhost", name="passbook Default Admin" | ||||
|     ) | ||||
|     pbadmin.set_password("pbadmin")  # noqa # nosec | ||||
|     pbadmin.is_superuser = True | ||||
|     pbadmin.is_staff = True | ||||
|     pbadmin.save() | ||||
|  | ||||
|  | ||||
| @ -27,5 +25,15 @@ class Migration(migrations.Migration): | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RemoveField(model_name="user", name="is_superuser",), | ||||
|         migrations.RemoveField(model_name="user", name="is_staff",), | ||||
|         migrations.RunPython(create_default_user), | ||||
|         migrations.AddField( | ||||
|             model_name="user", | ||||
|             name="is_superuser", | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="user", name="is_staff", field=models.BooleanField(default=False) | ||||
|         ), | ||||
|     ] | ||||
|  | ||||
							
								
								
									
										49
									
								
								passbook/core/migrations/0009_group_is_superuser.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								passbook/core/migrations/0009_group_is_superuser.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| # Generated by Django 3.1.1 on 2020-09-15 19:53 | ||||
| from django.apps.registry import Apps | ||||
| from django.db import migrations, models | ||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||
|  | ||||
| import passbook.core.models | ||||
|  | ||||
|  | ||||
| def create_default_admin_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     db_alias = schema_editor.connection.alias | ||||
|     Group = apps.get_model("passbook_core", "Group") | ||||
|     User = apps.get_model("passbook_core", "User") | ||||
|  | ||||
|     # Creates a default admin group | ||||
|     group, _ = Group.objects.using(db_alias).get_or_create( | ||||
|         is_superuser=True, defaults={"name": "passbook Admins",} | ||||
|     ) | ||||
|     group.users.set(User.objects.filter(username="pbadmin")) | ||||
|     group.save() | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("passbook_core", "0008_auto_20200824_1532"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RemoveField(model_name="user", name="is_superuser",), | ||||
|         migrations.RemoveField(model_name="user", name="is_staff",), | ||||
|         migrations.AlterField( | ||||
|             model_name="user", | ||||
|             name="pb_groups", | ||||
|             field=models.ManyToManyField( | ||||
|                 related_name="users", to="passbook_core.Group" | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="group", | ||||
|             name="is_superuser", | ||||
|             field=models.BooleanField( | ||||
|                 default=False, help_text="Users added to this group will be superusers." | ||||
|             ), | ||||
|         ), | ||||
|         migrations.RunPython(create_default_admin_group), | ||||
|         migrations.AlterModelManagers( | ||||
|             name="user", managers=[("objects", passbook.core.models.UserManager()),], | ||||
|         ), | ||||
|     ] | ||||
| @ -4,6 +4,7 @@ from typing import Any, Optional, Type | ||||
| from uuid import uuid4 | ||||
|  | ||||
| from django.contrib.auth.models import AbstractUser | ||||
| from django.contrib.auth.models import UserManager as DjangoUserManager | ||||
| from django.db import models | ||||
| from django.db.models import Q, QuerySet | ||||
| from django.forms import ModelForm | ||||
| @ -22,6 +23,7 @@ from passbook.lib.models import CreatedUpdatedModel | ||||
| from passbook.policies.models import PolicyBindingModel | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| PASSBOOK_USER_DEBUG = "passbook_user_debug" | ||||
|  | ||||
|  | ||||
| def default_token_duration(): | ||||
| @ -33,7 +35,12 @@ class Group(models.Model): | ||||
|     """Custom Group model which supports a basic hierarchy""" | ||||
|  | ||||
|     group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||
|  | ||||
|     name = models.CharField(_("name"), max_length=80) | ||||
|     is_superuser = models.BooleanField( | ||||
|         default=False, help_text=_("Users added to this group will be superusers.") | ||||
|     ) | ||||
|  | ||||
|     parent = models.ForeignKey( | ||||
|         "Group", | ||||
|         blank=True, | ||||
| @ -51,6 +58,14 @@ class Group(models.Model): | ||||
|         unique_together = (("name", "parent",),) | ||||
|  | ||||
|  | ||||
| class UserManager(DjangoUserManager): | ||||
|     """Custom user manager that doesn't assign is_superuser and is_staff""" | ||||
|  | ||||
|     def create_user(self, username, email=None, password=None, **extra_fields): | ||||
|         """Custom user manager that doesn't assign is_superuser and is_staff""" | ||||
|         return self._create_user(username, email, password, **extra_fields) | ||||
|  | ||||
|  | ||||
| class User(GuardianUserMixin, AbstractUser): | ||||
|     """Custom User model to allow easier adding o f user-based settings""" | ||||
|  | ||||
| @ -58,11 +73,23 @@ class User(GuardianUserMixin, AbstractUser): | ||||
|     name = models.TextField(help_text=_("User's display name.")) | ||||
|  | ||||
|     sources = models.ManyToManyField("Source", through="UserSourceConnection") | ||||
|     pb_groups = models.ManyToManyField("Group") | ||||
|     pb_groups = models.ManyToManyField("Group", related_name="users") | ||||
|     password_change_date = models.DateTimeField(auto_now_add=True) | ||||
|  | ||||
|     attributes = models.JSONField(default=dict, blank=True) | ||||
|  | ||||
|     objects = UserManager() | ||||
|  | ||||
|     @property | ||||
|     def is_superuser(self) -> bool: | ||||
|         """Get supseruser status based on membership in a group with superuser status""" | ||||
|         return self.pb_groups.filter(is_superuser=True).exists() | ||||
|  | ||||
|     @property | ||||
|     def is_staff(self) -> bool: | ||||
|         """superuser == staff user""" | ||||
|         return self.is_superuser | ||||
|  | ||||
|     def set_password(self, password): | ||||
|         if self.pk: | ||||
|             password_changed.send(sender=self, user=self, password=password) | ||||
| @ -92,6 +119,12 @@ class Provider(models.Model): | ||||
|  | ||||
|     objects = InheritanceManager() | ||||
|  | ||||
|     @property | ||||
|     def launch_url(self) -> Optional[str]: | ||||
|         """URL to this provider and initiate authorization for the user. | ||||
|         Can return None for providers that are not URL-based""" | ||||
|         return None | ||||
|  | ||||
|     def form(self) -> Type[ModelForm]: | ||||
|         """Return Form class used to edit this object""" | ||||
|         raise NotImplementedError | ||||
| @ -119,6 +152,14 @@ class Application(PolicyBindingModel): | ||||
|     meta_description = models.TextField(default="", blank=True) | ||||
|     meta_publisher = models.TextField(default="", blank=True) | ||||
|  | ||||
|     def get_launch_url(self) -> Optional[str]: | ||||
|         """Get launch URL if set, otherwise attempt to get launch URL based on provider.""" | ||||
|         if self.meta_launch_url: | ||||
|             return self.meta_launch_url | ||||
|         if self.provider: | ||||
|             return self.provider.launch_url | ||||
|         return None | ||||
|  | ||||
|     def get_provider(self) -> Optional[Provider]: | ||||
|         """Get casted provider instance""" | ||||
|         if not self.provider: | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| """passbook core tasks""" | ||||
| from django.utils.timezone import now | ||||
| from structlog import get_logger | ||||
|  | ||||
| from passbook.core.models import ExpiringModel | ||||
| @ -12,5 +13,10 @@ def clean_expired_models(): | ||||
|     """Remove expired objects""" | ||||
|     for cls in ExpiringModel.__subclasses__(): | ||||
|         cls: ExpiringModel | ||||
|         amount, _ = cls.filter_not_expired().delete() | ||||
|         amount, _ = ( | ||||
|             cls.objects.all() | ||||
|             .exclude(expiring=False) | ||||
|             .exclude(expiring=True, expires__gt=now()) | ||||
|             .delete() | ||||
|         ) | ||||
|         LOGGER.debug("Deleted expired models", model=cls, amount=amount) | ||||
|  | ||||
| @ -20,8 +20,12 @@ | ||||
|                 </button> | ||||
|             </div> | ||||
|             <a class="pf-c-page__header-brand-link"> | ||||
|                 <img class="pf-c-brand" src="{% static 'passbook/logo.png' %}" alt="" /> | ||||
|                 <img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" alt="passbook" /> | ||||
|                 <div class="pf-c-brand pb-brand"> | ||||
|                     <img src="{{ config.passbook.branding.logo }}" alt="passbook icon"> | ||||
|                     {% if config.passbook.branding.title_show %} | ||||
|                     <small><small>{{ config.passbook.branding.title }}</small></small> | ||||
|                     {% endif %} | ||||
|                 </div> | ||||
|             </a> | ||||
|         </div> | ||||
|         <div class="pf-c-page__header-nav"> | ||||
|  | ||||
| @ -6,15 +6,17 @@ | ||||
|  | ||||
| <html lang="en"> | ||||
|     <head> | ||||
|         <link rel="preload" href="{% static 'passbook/fonts/DINEngschriftStd.woff2' %}" as="font" type="font/woff2" crossorigin> | ||||
|         <link rel="preload" href="{% static 'passbook/fonts/DINEngschriftStd.woff' %}" as="font" type="font/woff" crossorigin> | ||||
|         <meta charset="UTF-8"> | ||||
|         <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> | ||||
|         <title>{% block title %}{% trans title|default:"passbook" %}{% endblock %}</title> | ||||
|         <title>{% block title %}{% trans title|default:config.passbook.branding.title %}{% endblock %}</title> | ||||
|         <link rel="icon" type="image/png" href="{% static 'passbook/logo.png' %}"> | ||||
|         <link rel="shortcut icon" type="image/png" href="{% static 'passbook/logo.png' %}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'node_modules/@patternfly/patternfly/patternfly.css' %}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'node_modules/@patternfly/patternfly/patternfly-addons.css' %}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css' %}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'passbook/pf.css' %}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'passbook/passbook.css' %}"> | ||||
|         {% block head %} | ||||
|         {% endblock %} | ||||
|     </head> | ||||
| @ -35,6 +37,6 @@ | ||||
|         {% endblock %} | ||||
|         {% block scripts %} | ||||
|         {% endblock %} | ||||
|         <script src="{% static 'passbook/pf.js' %}"></script> | ||||
|         <script src="{% static 'passbook/passbook.js' %}"></script> | ||||
|     </body> | ||||
| </html> | ||||
|  | ||||
| @ -22,8 +22,12 @@ | ||||
| <div class="pf-c-login"> | ||||
|     <div class="pf-c-login__container"> | ||||
|         <header class="pf-c-login__header"> | ||||
|             <img class="pf-c-brand" src="{% static 'passbook/logo.svg' %}" style="height: 60px;" alt="passbook icon" /> | ||||
|             <img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 60px;" alt="passbook branding" /> | ||||
|             <div class="pf-c-brand pb-brand"> | ||||
|                 <img src="{{ config.passbook.branding.logo }}" alt="passbook icon" /> | ||||
|                 {% if config.passbook.branding.title_show %} | ||||
|                 <p>{{ config.passbook.branding.title }}</p> | ||||
|                 {% endif %} | ||||
|             </div> | ||||
|         </header> | ||||
|         {% block main_container %} | ||||
|         <main class="pf-c-login__main"> | ||||
| @ -47,6 +51,13 @@ | ||||
|                     <a href="{{ link.href }}">{{ link.name }}</a> | ||||
|                 </li> | ||||
|                 {% endfor %} | ||||
|                 {% if config.passbook.branding.title != "passbook" %} | ||||
|                 <li> | ||||
|                     <a href="https://github.com/beryju/passbook"> | ||||
|                         {% trans 'Powered by passbook' %} | ||||
|                     </a> | ||||
|                 </li> | ||||
|                 {% endif %} | ||||
|             </ul> | ||||
|         </footer> | ||||
|     </div> | ||||
|  | ||||
| @ -1,29 +0,0 @@ | ||||
| {% extends 'login/base_full.html' %} | ||||
|  | ||||
| {% load static %} | ||||
| {% load i18n %} | ||||
| {% load passbook_utils %} | ||||
|  | ||||
| {% block card_title %} | ||||
| {% trans 'Permission denied' %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block title %} | ||||
| {% trans 'Permission denied' %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block card %} | ||||
|     <form method="POST" class="pf-c-form"> | ||||
|         {% csrf_token %} | ||||
|         {% include 'partials/form.html' %} | ||||
|         <div class="pf-c-form__group"> | ||||
|             <p> | ||||
|                 <i class="pf-icon pf-icon-error-circle-o"></i> | ||||
|                 {% trans 'Access denied' %} | ||||
|             </p> | ||||
|         </div> | ||||
|         {% if 'back' in request.GET %} | ||||
|         <a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a> | ||||
|         {% endif %} | ||||
|     </form> | ||||
| {% endblock %} | ||||
| @ -24,7 +24,7 @@ | ||||
|     {% if applications %} | ||||
|     <div class="pf-l-gallery pf-m-gutter"> | ||||
|         {% for app in applications %} | ||||
|         <a href="{{ app.meta_launch_url }}" class="pf-c-card pf-m-hoverable pf-m-compact"> | ||||
|         <a href="{{ app.get_launch_url }}" class="pf-c-card pf-m-hoverable pf-m-compact"> | ||||
|             <div class="pf-c-card__header"> | ||||
|                 {% if not app.meta_icon_url %} | ||||
|                 <i class="pf-icon pf-icon-arrow"></i> | ||||
|  | ||||
| @ -13,7 +13,7 @@ class TestOverviewViews(TestCase): | ||||
|  | ||||
|     def setUp(self): | ||||
|         super().setUp() | ||||
|         self.user = User.objects.create_superuser( | ||||
|         self.user = User.objects.create_user( | ||||
|             username="unittest user", | ||||
|             email="unittest@example.com", | ||||
|             password="".join( | ||||
|  | ||||
| @ -13,7 +13,7 @@ class TestUserViews(TestCase): | ||||
|  | ||||
|     def setUp(self): | ||||
|         super().setUp() | ||||
|         self.user = User.objects.create_superuser( | ||||
|         self.user = User.objects.create_user( | ||||
|             username="unittest user", | ||||
|             email="unittest@example.com", | ||||
|             password="".join( | ||||
|  | ||||
| @ -1,30 +0,0 @@ | ||||
| """passbook util view tests""" | ||||
| import string | ||||
| from random import SystemRandom | ||||
|  | ||||
| from django.test import RequestFactory, TestCase | ||||
|  | ||||
| from passbook.core.models import User | ||||
| from passbook.core.views.utils import PermissionDeniedView | ||||
|  | ||||
|  | ||||
| class TestUtilViews(TestCase): | ||||
|     """Test Utility Views""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         self.user = User.objects.create_superuser( | ||||
|             username="unittest user", | ||||
|             email="unittest@example.com", | ||||
|             password="".join( | ||||
|                 SystemRandom().choice(string.ascii_uppercase + string.digits) | ||||
|                 for _ in range(8) | ||||
|             ), | ||||
|         ) | ||||
|         self.factory = RequestFactory() | ||||
|  | ||||
|     def test_permission_denied_view(self): | ||||
|         """Test PermissionDeniedView""" | ||||
|         request = self.factory.get("something") | ||||
|         request.user = self.user | ||||
|         response = PermissionDeniedView.as_view()(request) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
| @ -1,14 +0,0 @@ | ||||
| """passbook core utils view""" | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.views.generic import TemplateView | ||||
|  | ||||
|  | ||||
| class PermissionDeniedView(TemplateView): | ||||
|     """Generic Permission denied view""" | ||||
|  | ||||
|     template_name = "login/denied.html" | ||||
|     title = _("Permission denied.") | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         kwargs["title"] = self.title | ||||
|         return super().get_context_data(**kwargs) | ||||
| @ -2,8 +2,8 @@ | ||||
|  | ||||
|  | ||||
| class FlowNonApplicableException(BaseException): | ||||
|     """Exception raised when a Flow does not apply to a user.""" | ||||
|     """Flow does not apply to current user (denied by policy).""" | ||||
|  | ||||
|  | ||||
| class EmptyFlowException(BaseException): | ||||
|     """Exception raised when a Flow Plan is empty""" | ||||
|     """Flow has no stages.""" | ||||
|  | ||||
| @ -4,7 +4,7 @@ from django.core.management.base import BaseCommand, no_translations | ||||
| from passbook.flows.transfer.importer import FlowImporter | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
| class Command(BaseCommand):  # pragma: no cover | ||||
|     """Apply flow from commandline""" | ||||
|  | ||||
|     @no_translations | ||||
|  | ||||
| @ -52,7 +52,7 @@ def create_default_source_enrollment_flow( | ||||
|  | ||||
|     # PromptStage to ask user for their username | ||||
|     prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create( | ||||
|         name="default-source-enrollment-username-prompt", | ||||
|         name="Welcome to passbook! Please select a username.", | ||||
|     ) | ||||
|     prompt, _ = Prompt.objects.using(db_alias).update_or_create( | ||||
|         field_key="username", | ||||
|  | ||||
| @ -115,11 +115,12 @@ const updateFormAction = (form) => { | ||||
|     for (let index = 0; index < form.elements.length; index++) { | ||||
|         const element = form.elements[index]; | ||||
|         if (element.value === form.action) { | ||||
|             console.log("Found Form action URL in form elements, not changing form action."); | ||||
|             console.log("pb-flow: Found Form action URL in form elements, not changing form action."); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|     form.action = flowBodyUrl; | ||||
|     console.log(`pb-flow: updated form.action ${flowBodyUrl}`); | ||||
|     return true; | ||||
| }; | ||||
| const checkAutosubmit = (form) => { | ||||
| @ -129,11 +130,11 @@ const checkAutosubmit = (form) => { | ||||
| }; | ||||
| const setFormSubmitHandlers = () => { | ||||
|     document.querySelectorAll("#flow-body form").forEach(form => { | ||||
|         console.log(`Checking for autosubmit attribute ${form}`); | ||||
|         console.log(`pb-flow: Checking for autosubmit attribute ${form}`); | ||||
|         checkAutosubmit(form); | ||||
|         console.log(`Setting action for form ${form}`); | ||||
|         console.log(`pb-flow: Setting action for form ${form}`); | ||||
|         updateFormAction(form); | ||||
|         console.log(`Adding handler for form ${form}`); | ||||
|         console.log(`pb-flow: Adding handler for form ${form}`); | ||||
|         form.addEventListener('submit', (e) => { | ||||
|             e.preventDefault(); | ||||
|             let formData = new FormData(form); | ||||
| @ -145,6 +146,7 @@ const setFormSubmitHandlers = () => { | ||||
|                 updateCard(data); | ||||
|             }); | ||||
|         }); | ||||
|         form.classList.add("pb-flow-wrapped"); | ||||
|     }); | ||||
| }; | ||||
|  | ||||
|  | ||||
| @ -1,9 +1,10 @@ | ||||
| """flow views tests""" | ||||
| from unittest.mock import MagicMock, PropertyMock, patch | ||||
|  | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.shortcuts import reverse | ||||
| from django.test import Client, TestCase | ||||
| from django.utils.encoding import force_text | ||||
| from django.utils.encoding import force_str | ||||
|  | ||||
| from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException | ||||
| from passbook.flows.markers import ReevaluateMarker, StageMarker | ||||
| @ -12,6 +13,7 @@ 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.http import AccessDeniedResponse | ||||
| from passbook.policies.models import PolicyBinding | ||||
| from passbook.policies.types import PolicyResult | ||||
| from passbook.stages.dummy.models import DummyStage | ||||
| @ -20,6 +22,15 @@ POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False)) | ||||
| POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True)) | ||||
|  | ||||
|  | ||||
| def to_stage_response(request: HttpRequest, source: HttpResponse): | ||||
|     """Mock for to_stage_response that returns the original response, so we can check | ||||
|     inheritance and member attributes""" | ||||
|     return source | ||||
|  | ||||
|  | ||||
| TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response) | ||||
|  | ||||
|  | ||||
| class TestFlowExecutor(TestCase): | ||||
|     """Test views logic""" | ||||
|  | ||||
| @ -48,9 +59,12 @@ class TestFlowExecutor(TestCase): | ||||
|                     "passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug} | ||||
|                 ), | ||||
|             ) | ||||
|             self.assertEqual(response.status_code, 400) | ||||
|             self.assertEqual(cancel_mock.call_count, 1) | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|             self.assertEqual(cancel_mock.call_count, 2) | ||||
|  | ||||
|     @patch( | ||||
|         "passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK, | ||||
|     ) | ||||
|     @patch( | ||||
|         "passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE, | ||||
|     ) | ||||
| @ -66,9 +80,13 @@ class TestFlowExecutor(TestCase): | ||||
|         response = self.client.get( | ||||
|             reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertIsInstance(response, AccessDeniedResponse) | ||||
|         self.assertInHTML(FlowNonApplicableException.__doc__, response.rendered_content) | ||||
|  | ||||
|     @patch( | ||||
|         "passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK, | ||||
|     ) | ||||
|     def test_invalid_empty_flow(self): | ||||
|         """Tests that an empty flow returns the correct error message""" | ||||
|         flow = Flow.objects.create( | ||||
| @ -81,7 +99,8 @@ class TestFlowExecutor(TestCase): | ||||
|         response = self.client.get( | ||||
|             reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertIsInstance(response, AccessDeniedResponse) | ||||
|         self.assertInHTML(EmptyFlowException.__doc__, response.rendered_content) | ||||
|  | ||||
|     def test_invalid_flow_redirect(self): | ||||
| @ -96,8 +115,10 @@ class TestFlowExecutor(TestCase): | ||||
|         dest = "/unique-string" | ||||
|         url = reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}) | ||||
|         response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}") | ||||
|         self.assertEqual(response.status_code, 302) | ||||
|         self.assertEqual(response.url, dest) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertJSONEqual( | ||||
|             force_str(response.content), {"type": "redirect", "to": dest}, | ||||
|         ) | ||||
|  | ||||
|     def test_multi_stage_flow(self): | ||||
|         """Test a full flow with multiple stages""" | ||||
| @ -247,7 +268,7 @@ class TestFlowExecutor(TestCase): | ||||
|         response = self.client.post(exec_url) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertJSONEqual( | ||||
|             force_text(response.content), | ||||
|             force_str(response.content), | ||||
|             {"type": "redirect", "to": reverse("passbook_core:overview")}, | ||||
|         ) | ||||
|  | ||||
| @ -293,7 +314,7 @@ class TestFlowExecutor(TestCase): | ||||
|             # First request, run the planner | ||||
|             response = self.client.get(exec_url) | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|             self.assertIn("dummy1", force_text(response.content)) | ||||
|             self.assertIn("dummy1", force_str(response.content)) | ||||
|  | ||||
|             plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] | ||||
|  | ||||
| @ -316,13 +337,13 @@ class TestFlowExecutor(TestCase): | ||||
|         # 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)) | ||||
|         self.assertIn("dummy4", force_str(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), | ||||
|             force_str(response.content), | ||||
|             {"type": "redirect", "to": reverse("passbook_core:overview")}, | ||||
|         ) | ||||
|  | ||||
| @ -6,12 +6,10 @@ from passbook.flows.views import ( | ||||
|     CancelView, | ||||
|     FlowExecutorShellView, | ||||
|     FlowExecutorView, | ||||
|     FlowPermissionDeniedView, | ||||
|     ToDefaultFlow, | ||||
| ) | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path("-/denied/", FlowPermissionDeniedView.as_view(), name="denied"), | ||||
|     path( | ||||
|         "-/default/authentication/", | ||||
|         ToDefaultFlow.as_view(designation=FlowDesignation.AUTHENTICATION), | ||||
|  | ||||
| @ -9,7 +9,7 @@ from django.http import ( | ||||
|     HttpResponseRedirect, | ||||
|     JsonResponse, | ||||
| ) | ||||
| from django.shortcuts import get_object_or_404, redirect, render, reverse | ||||
| from django.shortcuts import get_object_or_404, redirect, reverse | ||||
| from django.template.response import TemplateResponse | ||||
| from django.utils.decorators import method_decorator | ||||
| from django.views.decorators.clickjacking import xframe_options_sameorigin | ||||
| @ -17,13 +17,13 @@ from django.views.generic import TemplateView, View | ||||
| from structlog import get_logger | ||||
|  | ||||
| from passbook.audit.models import cleanse_dict | ||||
| from passbook.core.views.utils import PermissionDeniedView | ||||
| from passbook.core.models import PASSBOOK_USER_DEBUG | ||||
| from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException | ||||
| from passbook.flows.models import Flow, FlowDesignation, Stage | ||||
| from passbook.flows.planner import FlowPlan, FlowPlanner | ||||
| from passbook.lib.utils.reflection import class_to_path | ||||
| from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs | ||||
| from passbook.lib.views import bad_request_message | ||||
| from passbook.policies.http import AccessDeniedResponse | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| # Argument used to redirect user after login | ||||
| @ -54,7 +54,7 @@ class FlowExecutorView(View): | ||||
|                 LOGGER.debug("f(exec): Redirecting to next on fail") | ||||
|                 return redirect(self.request.GET.get(NEXT_ARG_NAME)) | ||||
|         message = exc.__doc__ if exc.__doc__ else str(exc) | ||||
|         return bad_request_message(self.request, message) | ||||
|         return self.stage_invalid(error_message=message) | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse: | ||||
|         # Early check if theres an active Plan for the current session | ||||
| @ -79,10 +79,10 @@ class FlowExecutorView(View): | ||||
|                 self.plan = self._initiate_plan() | ||||
|             except FlowNonApplicableException as exc: | ||||
|                 LOGGER.warning("f(exec): Flow not applicable to current user", exc=exc) | ||||
|                 return self.handle_invalid_flow(exc) | ||||
|                 return to_stage_response(self.request, self.handle_invalid_flow(exc)) | ||||
|             except EmptyFlowException as exc: | ||||
|                 LOGGER.warning("f(exec): Flow is empty", exc=exc) | ||||
|                 return self.handle_invalid_flow(exc) | ||||
|                 return to_stage_response(self.request, self.handle_invalid_flow(exc)) | ||||
|         # We don't save the Plan after getting the next stage | ||||
|         # as it hasn't been successfully passed yet | ||||
|         next_stage = self.plan.next() | ||||
| @ -115,14 +115,7 @@ class FlowExecutorView(View): | ||||
|             return to_stage_response(request, stage_response) | ||||
|         except Exception as exc:  # pylint: disable=broad-except | ||||
|             LOGGER.exception(exc) | ||||
|             return to_stage_response( | ||||
|                 request, | ||||
|                 render( | ||||
|                     request, | ||||
|                     "flows/error.html", | ||||
|                     {"error": exc, "tb": "".join(format_tb(exc.__traceback__))}, | ||||
|                 ), | ||||
|             ) | ||||
|             return to_stage_response(request, FlowErrorResponse(request, exc)) | ||||
|  | ||||
|     def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||
|         """pass post request to current stage""" | ||||
| @ -137,14 +130,7 @@ class FlowExecutorView(View): | ||||
|             return to_stage_response(request, stage_response) | ||||
|         except Exception as exc:  # pylint: disable=broad-except | ||||
|             LOGGER.exception(exc) | ||||
|             return to_stage_response( | ||||
|                 request, | ||||
|                 render( | ||||
|                     request, | ||||
|                     "flows/error.html", | ||||
|                     {"error": exc, "tb": "".join(format_tb(exc.__traceback__))}, | ||||
|                 ), | ||||
|             ) | ||||
|             return to_stage_response(request, FlowErrorResponse(request, exc)) | ||||
|  | ||||
|     def _initiate_plan(self) -> FlowPlan: | ||||
|         planner = FlowPlanner(self.flow) | ||||
| @ -193,12 +179,17 @@ class FlowExecutorView(View): | ||||
|         ) | ||||
|         return self._flow_done() | ||||
|  | ||||
|     def stage_invalid(self) -> HttpResponse: | ||||
|     def stage_invalid(self, error_message: Optional[str] = None) -> HttpResponse: | ||||
|         """Callback used stage when data is correct but a policy denies access | ||||
|         or the user account is disabled.""" | ||||
|         or the user account is disabled. | ||||
|  | ||||
|         Optionally, an exception can be passed, which will be shown if the current user | ||||
|         is a superuser.""" | ||||
|         LOGGER.debug("f(exec): Stage invalid", flow_slug=self.flow.slug) | ||||
|         self.cancel() | ||||
|         return redirect_with_qs("passbook_flows:denied", self.request.GET) | ||||
|         response = AccessDeniedResponse(self.request) | ||||
|         response.error_message = error_message | ||||
|         return response | ||||
|  | ||||
|     def cancel(self): | ||||
|         """Cancel current execution and return a redirect""" | ||||
| @ -212,8 +203,30 @@ class FlowExecutorView(View): | ||||
|                 del self.request.session[key] | ||||
|  | ||||
|  | ||||
| class FlowPermissionDeniedView(PermissionDeniedView): | ||||
|     """User could not be authenticated""" | ||||
| class FlowErrorResponse(TemplateResponse): | ||||
|     """Response class when an unhandled error occurs during a stage. Normal users | ||||
|     are shown an error message, superusers are shown a full stacktrace.""" | ||||
|  | ||||
|     error: Exception | ||||
|  | ||||
|     def __init__(self, request: HttpRequest, error: Exception) -> None: | ||||
|         # For some reason pyright complains about keyword argument usage here | ||||
|         # pyright: reportGeneralTypeIssues=false | ||||
|         super().__init__(request=request, template="flows/error.html") | ||||
|         self.error = error | ||||
|  | ||||
|     def resolve_context( | ||||
|         self, context: Optional[Dict[str, Any]] | ||||
|     ) -> Optional[Dict[str, Any]]: | ||||
|         if not context: | ||||
|             context = {} | ||||
|         context["error"] = self.error | ||||
|         if self._request.user and self._request.user.is_authenticated: | ||||
|             if self._request.user.is_superuser or self._request.user.attributes.get( | ||||
|                 PASSBOOK_USER_DEBUG, False | ||||
|             ): | ||||
|                 context["tb"] = "".join(format_tb(self.error.__traceback__)) | ||||
|         return context | ||||
|  | ||||
|  | ||||
| class FlowExecutorShellView(TemplateView): | ||||
|  | ||||
| @ -10,6 +10,7 @@ redis: | ||||
|   password: '' | ||||
|   cache_db: 0 | ||||
|   message_queue_db: 1 | ||||
|   ws_db: 2 | ||||
|  | ||||
| debug: false | ||||
| log_level: info | ||||
| @ -21,6 +22,10 @@ error_reporting: | ||||
|   send_pii: false | ||||
|  | ||||
| passbook: | ||||
|   branding: | ||||
|     title: passbook | ||||
|     title_show: true | ||||
|     logo: /static/passbook/logo.svg | ||||
|   # Optionally add links to the footer on the login page | ||||
|   footer_links: | ||||
|     - name: Documentation | ||||
|  | ||||
| @ -1,5 +1,9 @@ | ||||
| """Generic models""" | ||||
| import re | ||||
|  | ||||
| from django.core.validators import URLValidator | ||||
| from django.db import models | ||||
| from django.utils.regex_helper import _lazy_re_compile | ||||
| from model_utils.managers import InheritanceManager | ||||
| from rest_framework.serializers import BaseSerializer | ||||
|  | ||||
| @ -48,3 +52,21 @@ class InheritanceForeignKey(models.ForeignKey): | ||||
|     """Custom ForeignKey that uses InheritanceForwardManyToOneDescriptor""" | ||||
|  | ||||
|     forward_related_accessor_class = InheritanceForwardManyToOneDescriptor | ||||
|  | ||||
|  | ||||
| class DomainlessURLValidator(URLValidator): | ||||
|     """Subclass of URLValidator which doesn't check the domain | ||||
|     (to allow hostnames without domain)""" | ||||
|  | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.host_re = "(" + self.hostname_re + self.domain_re + "|localhost)" | ||||
|         self.regex = _lazy_re_compile( | ||||
|             r"^(?:[a-z0-9.+-]*)://"  # scheme is validated separately | ||||
|             r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?"  # user:pass authentication | ||||
|             r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")" | ||||
|             r"(?::\d{2,5})?"  # port | ||||
|             r"(?:[/?#][^\s]*)?"  # resource path | ||||
|             r"\Z", | ||||
|             re.IGNORECASE, | ||||
|         ) | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| """passbook sentry integration""" | ||||
| from billiard.exceptions import WorkerLostError | ||||
| from botocore.client import ClientError | ||||
| from celery.exceptions import CeleryError | ||||
| from django.core.exceptions import DisallowedHost, ValidationError | ||||
| from django.db import InternalError, OperationalError, ProgrammingError | ||||
| from django_redis.exceptions import ConnectionInterrupted | ||||
| @ -8,6 +9,7 @@ from redis.exceptions import ConnectionError as RedisConnectionError | ||||
| from redis.exceptions import RedisError | ||||
| from rest_framework.exceptions import APIException | ||||
| from structlog import get_logger | ||||
| from websockets.exceptions import WebSocketException | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
| @ -35,6 +37,8 @@ def before_send(event, hint): | ||||
|         OSError, | ||||
|         RedisError, | ||||
|         SentryIgnoredException, | ||||
|         WebSocketException, | ||||
|         CeleryError, | ||||
|     ) | ||||
|     if "exc_info" in hint: | ||||
|         _, exc_value, _ = hint["exc_info"] | ||||
|  | ||||
| @ -1,11 +1,5 @@ | ||||
| """passbook lib template utilities""" | ||||
| from django.template import Context, Template, loader | ||||
|  | ||||
|  | ||||
| def render_from_string(tmpl: str, ctx: Context) -> str: | ||||
|     """Render template from string to string""" | ||||
|     template = Template(tmpl) | ||||
|     return template.render(ctx) | ||||
| from django.template import Context, loader | ||||
|  | ||||
|  | ||||
| def render_to_string(template_path: str, ctx: Context) -> str: | ||||
|  | ||||
| @ -27,12 +27,12 @@ class CreateAssignPermView(CreateView): | ||||
|  | ||||
|  | ||||
| def bad_request_message( | ||||
|     request: HttpRequest, message: str, title="Bad Request" | ||||
|     request: HttpRequest, | ||||
|     message: str, | ||||
|     title="Bad Request", | ||||
|     template="error/generic.html", | ||||
| ) -> TemplateResponse: | ||||
|     """Return generic error page with message, with status code set to 400""" | ||||
|     return TemplateResponse( | ||||
|         request, | ||||
|         "error/generic.html", | ||||
|         {"message": message, "card_title": _(title)}, | ||||
|         status=400, | ||||
|         request, template, {"message": message, "card_title": _(title)}, status=400, | ||||
|     ) | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| """Kubernetes deployment controller""" | ||||
| from base64 import b64encode | ||||
| from io import StringIO | ||||
|  | ||||
| from kubernetes.client import ( | ||||
| @ -24,6 +25,11 @@ from passbook import __version__ | ||||
| from passbook.outposts.controllers.base import BaseController | ||||
|  | ||||
|  | ||||
| def b64encode_str(input_string: str) -> str: | ||||
|     """base64 encode string""" | ||||
|     return b64encode(input_string.encode()).decode() | ||||
|  | ||||
|  | ||||
| class KubernetesController(BaseController): | ||||
|     """Manage deployment of outpost in kubernetes""" | ||||
|  | ||||
| @ -37,9 +43,9 @@ class KubernetesController(BaseController): | ||||
|         with StringIO() as _str: | ||||
|             dump_all( | ||||
|                 [ | ||||
|                     self.get_deployment_secret(), | ||||
|                     self.get_deployment(), | ||||
|                     self.get_service(), | ||||
|                     self.get_deployment_secret().to_dict(), | ||||
|                     self.get_deployment().to_dict(), | ||||
|                     self.get_service().to_dict(), | ||||
|                 ], | ||||
|                 stream=_str, | ||||
|                 default_flow_style=False, | ||||
| @ -63,15 +69,18 @@ class KubernetesController(BaseController): | ||||
|     def get_deployment_secret(self) -> V1Secret: | ||||
|         """Get secret with token and passbook host""" | ||||
|         return V1Secret( | ||||
|             api_version="v1", | ||||
|             kind="secret", | ||||
|             type="Opaque", | ||||
|             metadata=self.get_object_meta( | ||||
|                 name=f"passbook-outpost-{self.outpost.name}-api" | ||||
|             ), | ||||
|             data={ | ||||
|                 "passbook_host": self.outpost.config.passbook_host, | ||||
|                 "passbook_host_insecure": str( | ||||
|                     self.outpost.config.passbook_host_insecure | ||||
|                 "passbook_host": b64encode_str(self.outpost.config.passbook_host), | ||||
|                 "passbook_host_insecure": b64encode_str( | ||||
|                     str(self.outpost.config.passbook_host_insecure) | ||||
|                 ), | ||||
|                 "token": self.outpost.token.token_uuid.hex, | ||||
|                 "token": b64encode_str(self.outpost.token.token_uuid.hex), | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
| @ -82,6 +91,8 @@ class KubernetesController(BaseController): | ||||
|         for port_name, port in self.deployment_ports.items(): | ||||
|             ports.append(V1ServicePort(name=port_name, port=port)) | ||||
|         return V1Service( | ||||
|             api_version="v1", | ||||
|             kind="service", | ||||
|             metadata=meta, | ||||
|             spec=V1ServiceSpec(ports=ports, selector=meta.labels, type="ClusterIP"), | ||||
|         ) | ||||
| @ -94,6 +105,8 @@ class KubernetesController(BaseController): | ||||
|             container_ports.append(V1ContainerPort(container_port=port, name=port_name)) | ||||
|         meta = self.get_object_meta(name=f"passbook-outpost-{self.outpost.name}") | ||||
|         return V1Deployment( | ||||
|             api_version="apps/v1", | ||||
|             kind="deployment", | ||||
|             metadata=meta, | ||||
|             spec=V1DeploymentSpec( | ||||
|                 replicas=1, | ||||
|  | ||||
| @ -1,15 +1,16 @@ | ||||
| """Outpost models""" | ||||
| from dataclasses import asdict, dataclass | ||||
| from datetime import datetime | ||||
| from json import dumps, loads | ||||
| from typing import Iterable, Optional | ||||
| from uuid import uuid4 | ||||
|  | ||||
| from dacite import from_dict | ||||
| from django.contrib.postgres.fields import ArrayField | ||||
| from django.core.cache import cache | ||||
| from django.db import models | ||||
| from django.db import models, transaction | ||||
| from django.db.models.base import Model | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from guardian.models import UserObjectPermission | ||||
| from guardian.shortcuts import assign_perm | ||||
|  | ||||
| from passbook.core.models import Provider, Token, TokenIntents, User | ||||
| @ -30,13 +31,17 @@ class OutpostConfig: | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class OutpostModel: | ||||
| class OutpostModel(Model): | ||||
|     """Base model for providers that need more objects than just themselves""" | ||||
|  | ||||
|     def get_required_objects(self) -> Iterable[models.Model]: | ||||
|         """Return a list of all required objects""" | ||||
|         return [self] | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         abstract = True | ||||
|  | ||||
|  | ||||
| class OutpostType(models.TextChoices): | ||||
|     """Outpost types, currently only the reverse proxy is available""" | ||||
| @ -79,12 +84,12 @@ class Outpost(models.Model): | ||||
|     @property | ||||
|     def config(self) -> OutpostConfig: | ||||
|         """Load config as OutpostConfig object""" | ||||
|         return from_dict(OutpostConfig, loads(self._config)) | ||||
|         return from_dict(OutpostConfig, self._config) | ||||
|  | ||||
|     @config.setter | ||||
|     def config(self, value): | ||||
|         """Dump config into json""" | ||||
|         self._config = dumps(asdict(value)) | ||||
|         self._config = asdict(value) | ||||
|  | ||||
|     @property | ||||
|     def health_cache_key(self) -> str: | ||||
| @ -100,24 +105,24 @@ class Outpost(models.Model): | ||||
|             return datetime.fromtimestamp(value) | ||||
|         return None | ||||
|  | ||||
|     def _create_user(self) -> User: | ||||
|         """Create user and assign permissions for all required objects""" | ||||
|         user: User = User.objects.create(username=f"pb-outpost-{self.uuid.hex}") | ||||
|         user.set_unusable_password() | ||||
|         user.save() | ||||
|         for model in self.get_required_objects(): | ||||
|             assign_perm( | ||||
|                 f"{model._meta.app_label}.view_{model._meta.model_name}", user, model | ||||
|             ) | ||||
|         return user | ||||
|  | ||||
|     @property | ||||
|     def user(self) -> User: | ||||
|         """Get/create user with access to all required objects""" | ||||
|         user = User.objects.filter(username=f"pb-outpost-{self.uuid.hex}") | ||||
|         if user.exists(): | ||||
|             return user.first() | ||||
|         return self._create_user() | ||||
|         users = User.objects.filter(username=f"pb-outpost-{self.uuid.hex}") | ||||
|         if not users.exists(): | ||||
|             user: User = User.objects.create(username=f"pb-outpost-{self.uuid.hex}") | ||||
|             user.set_unusable_password() | ||||
|             user.save() | ||||
|         else: | ||||
|             user = users.first() | ||||
|         # To ensure the user only has the correct permissions, we delete all of them and re-add | ||||
|         # the ones the user needs | ||||
|         with transaction.atomic(): | ||||
|             UserObjectPermission.objects.filter(user=user).delete() | ||||
|             for model in self.get_required_objects(): | ||||
|                 code_name = f"{model._meta.app_label}.view_{model._meta.model_name}" | ||||
|                 assign_perm(code_name, user, model) | ||||
|         return user | ||||
|  | ||||
|     @property | ||||
|     def token(self) -> Token: | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| """Outposts Settings""" | ||||
| from celery.schedules import crontab | ||||
| # from celery.schedules import crontab | ||||
|  | ||||
| CELERY_BEAT_SCHEDULE = { | ||||
|     "outposts_k8s": { | ||||
|         "task": "passbook.outposts.tasks.outpost_k8s_controller", | ||||
|         "schedule": crontab(minute="*/5"),  # Run every 5 minutes | ||||
|         "options": {"queue": "passbook_scheduled"}, | ||||
|     } | ||||
| } | ||||
| # CELERY_BEAT_SCHEDULE = { | ||||
| #     "outposts_k8s": { | ||||
| #         "task": "passbook.outposts.tasks.outpost_k8s_controller", | ||||
| #         "schedule": crontab(minute="*/5"),  # Run every 5 minutes | ||||
| #         "options": {"queue": "passbook_scheduled"}, | ||||
| #     } | ||||
| # } | ||||
|  | ||||
| @ -1,31 +1,31 @@ | ||||
| """passbook outpost signals""" | ||||
| from asgiref.sync import async_to_sync | ||||
| from channels.layers import get_channel_layer | ||||
| from django.db.models import Model | ||||
| from django.db.models.signals import post_save | ||||
| from django.dispatch import receiver | ||||
| from structlog import get_logger | ||||
|  | ||||
| from passbook.lib.utils.reflection import class_to_path | ||||
| from passbook.outposts.models import Outpost, OutpostModel | ||||
| from passbook.outposts.tasks import outpost_send_update | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @receiver(post_save, sender=Outpost) | ||||
| # pylint: disable=unused-argument | ||||
| def ensure_user_and_token(sender, instance, **_): | ||||
| def ensure_user_and_token(sender, instance: Model, **_): | ||||
|     """Ensure that token is created/updated on save""" | ||||
|     _ = instance.token | ||||
|  | ||||
|  | ||||
| @receiver(post_save) | ||||
| # pylint: disable=unused-argument | ||||
| def post_save_update(sender, instance, **_): | ||||
| def post_save_update(sender, instance: Model, **_): | ||||
|     """If an OutpostModel, or a model that is somehow connected to an OutpostModel is saved, | ||||
|     we send a message down the relevant OutpostModels WS connection to trigger an update""" | ||||
|     if isinstance(instance, OutpostModel): | ||||
|         LOGGER.debug("triggering outpost update from outpostmodel", instance=instance) | ||||
|         _send_update(instance) | ||||
|         outpost_send_update.delay(class_to_path(instance.__class__), instance.pk) | ||||
|         return | ||||
|  | ||||
|     for field in instance._meta.get_fields(): | ||||
| @ -46,13 +46,4 @@ def post_save_update(sender, instance, **_): | ||||
|         # Because the Outpost Model has an M2M to Provider, | ||||
|         # we have to iterate over the entire QS | ||||
|         for reverse in getattr(instance, field_name).all(): | ||||
|             _send_update(reverse) | ||||
|  | ||||
|  | ||||
| def _send_update(outpost_model: Model): | ||||
|     """Send update trigger for each channel of an outpost model""" | ||||
|     for outpost in outpost_model.outpost_set.all(): | ||||
|         channel_layer = get_channel_layer() | ||||
|         for channel in outpost.channels: | ||||
|             LOGGER.debug("sending update", channel=channel) | ||||
|             async_to_sync(channel_layer.send)(channel, {"type": "event.update"}) | ||||
|             outpost_send_update(class_to_path(reverse.__class__), reverse.pk) | ||||
|  | ||||
| @ -1,8 +1,22 @@ | ||||
| """outpost tasks""" | ||||
| from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType | ||||
| from typing import Any | ||||
|  | ||||
| from asgiref.sync import async_to_sync | ||||
| from channels.layers import get_channel_layer | ||||
| from structlog import get_logger | ||||
|  | ||||
| from passbook.lib.utils.reflection import path_to_class | ||||
| from passbook.outposts.models import ( | ||||
|     Outpost, | ||||
|     OutpostDeploymentType, | ||||
|     OutpostModel, | ||||
|     OutpostType, | ||||
| ) | ||||
| from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController | ||||
| from passbook.root.celery import CELERY_APP | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True) | ||||
| # pylint: disable=unused-argument | ||||
| @ -20,3 +34,16 @@ def outpost_k8s_controller_single(self, outpost: str, outpost_type: str): | ||||
|     """Launch Kubernetes manager and reconcile deployment/service/etc""" | ||||
|     if outpost_type == OutpostType.PROXY: | ||||
|         ProxyKubernetesController(outpost).run() | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task() | ||||
| def outpost_send_update(model_class: str, model_pk: Any): | ||||
|     """Send outpost update to all registered outposts, irregardless to which passbook | ||||
|     instance they are connected""" | ||||
|     model = path_to_class(model_class) | ||||
|     outpost_model: OutpostModel = model.objects.get(pk=model_pk) | ||||
|     for outpost in outpost_model.outpost_set.all(): | ||||
|         channel_layer = get_channel_layer() | ||||
|         for channel in outpost.channels: | ||||
|             LOGGER.debug("sending update", channel=channel) | ||||
|             async_to_sync(channel_layer.send)(channel, {"type": "event.update"}) | ||||
|  | ||||
							
								
								
									
										60
									
								
								passbook/outposts/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								passbook/outposts/tests.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| """outpost tests""" | ||||
| from django.test import TestCase | ||||
| from guardian.models import UserObjectPermission | ||||
|  | ||||
| from passbook.crypto.models import CertificateKeyPair | ||||
| from passbook.flows.models import Flow | ||||
| from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType | ||||
| from passbook.providers.proxy.models import ProxyProvider | ||||
|  | ||||
|  | ||||
| class OutpostTests(TestCase): | ||||
|     """Outpost Tests""" | ||||
|  | ||||
|     def test_service_account_permissions(self): | ||||
|         """Test that the service account has correct permissions""" | ||||
|         provider: ProxyProvider = ProxyProvider.objects.create( | ||||
|             name="test", | ||||
|             internal_host="http://localhost", | ||||
|             external_host="http://localhost", | ||||
|             authorization_flow=Flow.objects.first(), | ||||
|         ) | ||||
|         outpost: Outpost = Outpost.objects.create( | ||||
|             name="test", | ||||
|             type=OutpostType.PROXY, | ||||
|             deployment_type=OutpostDeploymentType.CUSTOM, | ||||
|         ) | ||||
|  | ||||
|         # Before we add a provider, the user should only have access to the outpost | ||||
|         permissions = UserObjectPermission.objects.filter(user=outpost.user) | ||||
|         self.assertEqual(len(permissions), 1) | ||||
|         self.assertEqual(permissions[0].object_pk, str(outpost.pk)) | ||||
|  | ||||
|         # We add a provider, user should only have access to outpost and provider | ||||
|         outpost.providers.add(provider) | ||||
|         outpost.save() | ||||
|         permissions = UserObjectPermission.objects.filter(user=outpost.user).order_by( | ||||
|             "content_type__model" | ||||
|         ) | ||||
|         self.assertEqual(len(permissions), 2) | ||||
|         self.assertEqual(permissions[0].object_pk, str(outpost.pk)) | ||||
|         self.assertEqual(permissions[1].object_pk, str(provider.pk)) | ||||
|  | ||||
|         # Provider requires a certificate-key-pair, user should have permissions for it | ||||
|         keypair = CertificateKeyPair.objects.first() | ||||
|         provider.certificate = keypair | ||||
|         provider.save() | ||||
|         permissions = UserObjectPermission.objects.filter(user=outpost.user).order_by( | ||||
|             "content_type__model" | ||||
|         ) | ||||
|         self.assertEqual(len(permissions), 3) | ||||
|         self.assertEqual(permissions[0].object_pk, str(keypair.pk)) | ||||
|         self.assertEqual(permissions[1].object_pk, str(outpost.pk)) | ||||
|         self.assertEqual(permissions[2].object_pk, str(provider.pk)) | ||||
|  | ||||
|         # Remove provider from outpost, user should only have access to outpost | ||||
|         outpost.providers.remove(provider) | ||||
|         outpost.save() | ||||
|         permissions = UserObjectPermission.objects.filter(user=outpost.user) | ||||
|         self.assertEqual(len(permissions), 1) | ||||
|         self.assertEqual(permissions[0].object_pk, str(outpost.pk)) | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	