Proxy v2 (#189)
This commit is contained in:
		| @ -1,7 +1,7 @@ | ||||
| [run] | ||||
| source = passbook | ||||
| omit = | ||||
|     */wsgi.py | ||||
|     */asgi.py | ||||
|     manage.py | ||||
|     */migrations/* | ||||
|     */apps.py | ||||
|  | ||||
							
								
								
									
										12
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @ -23,7 +23,7 @@ jobs: | ||||
|         run: docker push beryju/passbook:0.9.0-stable | ||||
|       - name: Push Docker Container to Registry (latest) | ||||
|         run: docker push beryju/passbook:latest | ||||
|   build-gatekeeper: | ||||
|   build-proxy: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v1 | ||||
| @ -34,16 +34,16 @@ jobs: | ||||
|         run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD | ||||
|       - name: Building Docker Image | ||||
|         run: | | ||||
|           cd gatekeeper | ||||
|           cd proxy | ||||
|           docker build \ | ||||
|           --no-cache \ | ||||
|           -t beryju/passbook-gatekeeper:0.9.0-stable \ | ||||
|           -t beryju/passbook-gatekeeper:latest \ | ||||
|           -t beryju/passbook-proxy:0.9.0-stable \ | ||||
|           -t beryju/passbook-proxy:latest \ | ||||
|           -f Dockerfile . | ||||
|       - name: Push Docker Container to Registry (versioned) | ||||
|         run: docker push beryju/passbook-gatekeeper:0.9.0-stable | ||||
|         run: docker push beryju/passbook-proxy:0.9.0-stable | ||||
|       - name: Push Docker Container to Registry (latest) | ||||
|         run: docker push beryju/passbook-gatekeeper:latest | ||||
|         run: docker push beryju/passbook-proxy:latest | ||||
|   build-static: | ||||
|     runs-on: ubuntu-latest | ||||
|     services: | ||||
|  | ||||
| @ -17,14 +17,16 @@ COPY --from=locker /app/requirements-dev.txt /app/ | ||||
| WORKDIR /app/ | ||||
|  | ||||
| RUN apt-get update && \ | ||||
|     apt-get install -y --no-install-recommends postgresql-client-11 && \ | ||||
|     apt-get install -y --no-install-recommends postgresql-client-11 build-essential && \ | ||||
|     rm -rf /var/lib/apt/ && \ | ||||
|     pip install -r requirements.txt  --no-cache-dir && \ | ||||
|     apt-get remove --purge -y build-essential && \ | ||||
|     apt-get autoremove --purge && \ | ||||
|     adduser --system --no-create-home --uid 1000 --group --home /app passbook | ||||
|  | ||||
| COPY ./passbook/ /app/passbook | ||||
| COPY ./manage.py /app/ | ||||
| COPY ./docker/uwsgi.ini /app/ | ||||
| COPY ./docker/gunicorn.conf.py /app/ | ||||
| COPY ./docker/bootstrap.sh /bootstrap.sh | ||||
| COPY ./docker/wait_for_db.py /app/wait_for_db.py | ||||
|  | ||||
|  | ||||
							
								
								
									
										20
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| all: lint-fix lint coverage gen | ||||
|  | ||||
| coverage: | ||||
| 	coverage run --concurrency=multiprocessing manage.py test passbook --failfast | ||||
| 	coverage combine | ||||
| 	coverage html | ||||
| 	coverage report | ||||
|  | ||||
| lint-fix: | ||||
| 	isort -rc . | ||||
| 	black . | ||||
|  | ||||
| lint: | ||||
| 	pyright | ||||
| 	bandit -r . | ||||
| 	pylint passbook | ||||
| 	prospector | ||||
|  | ||||
| gen: coverage | ||||
| 	./manage.py generate_swagger -o swagger.yaml -f yaml | ||||
							
								
								
									
										6
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								Pipfile
									
									
									
									
									
								
							| @ -28,7 +28,8 @@ packaging = "*" | ||||
| psycopg2-binary = "*" | ||||
| pycryptodome = "*" | ||||
| pyjwkest = "*" | ||||
| pyuwsgi = "*" | ||||
| uvicorn = "*" | ||||
| gunicorn = "*" | ||||
| pyyaml = "*" | ||||
| qrcode = "*" | ||||
| requests-oauthlib = "*" | ||||
| @ -39,6 +40,9 @@ structlog = "*" | ||||
| swagger-spec-validator = "*" | ||||
| urllib3 = {extras = ["secure"],version = "*"} | ||||
| dacite = "*" | ||||
| channels = "*" | ||||
| channels-redis = "*" | ||||
| kubernetes = "*" | ||||
|  | ||||
| [requires] | ||||
| python_version = "3.8" | ||||
|  | ||||
							
								
								
									
										488
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										488
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @ -1,7 +1,7 @@ | ||||
| { | ||||
|     "_meta": { | ||||
|         "hash": { | ||||
|             "sha256": "8f099b73d5993a0693261bf3d2b0e696d4f4d7ddd69a10d3db8ffe59a8ebd805" | ||||
|             "sha256": "a798bbd0b97857cac136c1743b8d6ad8bf8c3d95e2760c71d324bb2a7f47f678" | ||||
|         }, | ||||
|         "pipfile-spec": 6, | ||||
|         "requires": { | ||||
| @ -16,11 +16,19 @@ | ||||
|         ] | ||||
|     }, | ||||
|     "default": { | ||||
|         "aioredis": { | ||||
|             "hashes": [ | ||||
|                 "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a", | ||||
|                 "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3" | ||||
|             ], | ||||
|             "version": "==1.3.1" | ||||
|         }, | ||||
|         "amqp": { | ||||
|             "hashes": [ | ||||
|                 "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21", | ||||
|                 "sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||
|             "version": "==2.6.1" | ||||
|         }, | ||||
|         "asgiref": { | ||||
| @ -28,15 +36,40 @@ | ||||
|                 "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", | ||||
|                 "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==3.2.10" | ||||
|         }, | ||||
|         "async-timeout": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", | ||||
|                 "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" | ||||
|             ], | ||||
|             "markers": "python_full_version >= '3.5.3'", | ||||
|             "version": "==3.0.1" | ||||
|         }, | ||||
|         "attrs": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", | ||||
|                 "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==20.1.0" | ||||
|         }, | ||||
|         "autobahn": { | ||||
|             "hashes": [ | ||||
|                 "sha256:24ce276d313e84d68241c3aef30d484f352b90a40168981b3640312c821df77b", | ||||
|                 "sha256:86bbce30cdd407137c57670993a8f9bfdfe3f8e994b889181d85e844d5aa8dfb" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==20.7.1" | ||||
|         }, | ||||
|         "automat": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33", | ||||
|                 "sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111" | ||||
|             ], | ||||
|             "version": "==20.2.0" | ||||
|         }, | ||||
|         "billiard": { | ||||
|             "hashes": [ | ||||
|                 "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede", | ||||
| @ -46,17 +79,26 @@ | ||||
|         }, | ||||
|         "boto3": { | ||||
|             "hashes": [ | ||||
|                 "sha256:b240ac281de363e25a8e1a4c862559d6a056d98dcb9f487fc94d73c6f6599dfc" | ||||
|                 "sha256:4196b418598851ffd10cf9d1606694673cbfeca4ddf8b25d4e50addbd2fc60bf", | ||||
|                 "sha256:69ad8f2184979e223e12ee3071674fdf910983cf9f4d6f34f7ec407b089064b5" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.14.53" | ||||
|             "version": "==1.14.54" | ||||
|         }, | ||||
|         "botocore": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7e0272ceeb7747ed259a392e8d7b624cfd037085a8c59ef2b9f8916e7c556267", | ||||
|                 "sha256:d37a83ac23257c85c48b74ab81173980234f8fc078e7a9d312d0ee7d057f90e6" | ||||
|                 "sha256:6fe05837646447d61acdaf1e3401b92cd9309f00b19c577a50d0ade7735a3403", | ||||
|                 "sha256:9e493a21e6a8d45c631eb2952ae8e1d0a31b9984546d4268ea10c0c33e2435ce" | ||||
|             ], | ||||
|             "version": "==1.17.53" | ||||
|             "version": "==1.17.54" | ||||
|         }, | ||||
|         "cachetools": { | ||||
|             "hashes": [ | ||||
|                 "sha256:513d4ff98dd27f85743a8dc0e92f55ddb1b49e060c2d5961512855cda2c01a98", | ||||
|                 "sha256:bbaa39c3dede00175df2dc2b03d0cf18dd2d32a7de7beb68072d13043c9edb20" | ||||
|             ], | ||||
|             "markers": "python_version ~= '3.5'", | ||||
|             "version": "==4.1.1" | ||||
|         }, | ||||
|         "celery": { | ||||
|             "hashes": [ | ||||
| @ -106,6 +148,22 @@ | ||||
|             ], | ||||
|             "version": "==1.14.2" | ||||
|         }, | ||||
|         "channels": { | ||||
|             "hashes": [ | ||||
|                 "sha256:08e756406d7165cb32f6fc3090c0643f41ca9f7e0f7fada0b31194662f20f414", | ||||
|                 "sha256:80a5ad1962ae039a3dcc0a5cb5212413e66e2f11ad9e9db8004834436daf3400" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==2.4.0" | ||||
|         }, | ||||
|         "channels-redis": { | ||||
|             "hashes": [ | ||||
|                 "sha256:b4bcee949032cd838abdffd10da056930fca1a5a7ebc52139f8537aa622ac8d5", | ||||
|                 "sha256:be7c14526ab924a091a66ad72a8be57a34900440b1126d520ac7742c0e2add03" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.0.1" | ||||
|         }, | ||||
|         "chardet": { | ||||
|             "hashes": [ | ||||
|                 "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", | ||||
| @ -113,6 +171,21 @@ | ||||
|             ], | ||||
|             "version": "==3.0.4" | ||||
|         }, | ||||
|         "click": { | ||||
|             "hashes": [ | ||||
|                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", | ||||
|                 "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||
|             "version": "==7.1.2" | ||||
|         }, | ||||
|         "constantly": { | ||||
|             "hashes": [ | ||||
|                 "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", | ||||
|                 "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d" | ||||
|             ], | ||||
|             "version": "==15.1.0" | ||||
|         }, | ||||
|         "coreapi": { | ||||
|             "hashes": [ | ||||
|                 "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb", | ||||
| @ -159,6 +232,13 @@ | ||||
|             "index": "pypi", | ||||
|             "version": "==1.5.1" | ||||
|         }, | ||||
|         "daphne": { | ||||
|             "hashes": [ | ||||
|                 "sha256:1ca46d7419103958bbc9576fb7ba3b25b053006e22058bc97084ee1a7d44f4ba", | ||||
|                 "sha256:aa64840015709bbc9daa3c4464a4a4d437937d6cda10a9b51e913eb319272553" | ||||
|             ], | ||||
|             "version": "==2.5.0" | ||||
|         }, | ||||
|         "defusedxml": { | ||||
|             "hashes": [ | ||||
|                 "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", | ||||
| @ -265,6 +345,7 @@ | ||||
|                 "sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32", | ||||
|                 "sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==3.11.1" | ||||
|         }, | ||||
|         "djangorestframework-guardian": { | ||||
| @ -281,6 +362,7 @@ | ||||
|                 "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", | ||||
|                 "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==0.15.2" | ||||
|         }, | ||||
|         "drf-yasg": { | ||||
| @ -310,8 +392,109 @@ | ||||
|             "hashes": [ | ||||
|                 "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==0.18.2" | ||||
|         }, | ||||
|         "google-auth": { | ||||
|             "hashes": [ | ||||
|                 "sha256:982e1f82cace752134660b4c0ff660761b32146a55abb3ad6d225529012af87c", | ||||
|                 "sha256:f2498ad9cac3d2942d6c509ba18c4639656b366681881a1805f44f2a0c2d46f1" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==1.21.0" | ||||
|         }, | ||||
|         "gunicorn": { | ||||
|             "hashes": [ | ||||
|                 "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", | ||||
|                 "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==20.0.4" | ||||
|         }, | ||||
|         "h11": { | ||||
|             "hashes": [ | ||||
|                 "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1", | ||||
|                 "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1" | ||||
|             ], | ||||
|             "version": "==0.9.0" | ||||
|         }, | ||||
|         "hiredis": { | ||||
|             "hashes": [ | ||||
|                 "sha256:06a039208f83744a702279b894c8cf24c14fd63c59cd917dcde168b79eef0680", | ||||
|                 "sha256:0a909bf501459062aa1552be1461456518f367379fdc9fdb1f2ca5e4a1fdd7c0", | ||||
|                 "sha256:18402d9e54fb278cb9a8c638df6f1550aca36a009d47ecf5aa263a38600f35b0", | ||||
|                 "sha256:1e4cbbc3858ec7e680006e5ca590d89a5e083235988f26a004acf7244389ac01", | ||||
|                 "sha256:23344e3c2177baf6975fbfa361ed92eb7d36d08f454636e5054b3faa7c2aff8a", | ||||
|                 "sha256:289b31885b4996ce04cadfd5fc03d034dce8e2a8234479f7c9e23b9e245db06b", | ||||
|                 "sha256:2c1c570ae7bf1bab304f29427e2475fe1856814312c4a1cf1cd0ee133f07a3c6", | ||||
|                 "sha256:2c227c0ed371771ffda256034427320870e8ea2e4fd0c0a618c766e7c49aad73", | ||||
|                 "sha256:3bb9b63d319402cead8bbd9dd55dca3b667d2997e9a0d8a1f9b6cc274db4baee", | ||||
|                 "sha256:3ef2183de67b59930d2db8b8e8d4d58e00a50fcc5e92f4f678f6eed7a1c72d55", | ||||
|                 "sha256:43b8ed3dbfd9171e44c554cb4acf4ee4505caa84c5e341858b50ea27dd2b6e12", | ||||
|                 "sha256:47bcf3c5e6c1e87ceb86cdda2ee983fa0fe56a999e6185099b3c93a223f2fa9b", | ||||
|                 "sha256:5263db1e2e1e8ae30500cdd75a979ff99dcc184201e6b4b820d0de74834d2323", | ||||
|                 "sha256:5b1451727f02e7acbdf6aae4e06d75f66ee82966ff9114550381c3271a90f56c", | ||||
|                 "sha256:6996883a8a6ff9117cbb3d6f5b0dcbbae6fb9e31e1a3e4e2f95e0214d9a1c655", | ||||
|                 "sha256:6c96f64a54f030366657a54bb90b3093afc9c16c8e0dfa29fc0d6dbe169103a5", | ||||
|                 "sha256:7332d5c3e35154cd234fd79573736ddcf7a0ade7a986db35b6196b9171493e75", | ||||
|                 "sha256:7885b6f32c4a898e825bb7f56f36a02781ac4a951c63e4169f0afcf9c8c30dfb", | ||||
|                 "sha256:7b0f63f10a166583ab744a58baad04e0f52cfea1ac27bfa1b0c21a48d1003c23", | ||||
|                 "sha256:819f95d4eba3f9e484dd115ab7ab72845cf766b84286a00d4ecf76d33f1edca1", | ||||
|                 "sha256:8968eeaa4d37a38f8ca1f9dbe53526b69628edc9c42229a5b2f56d98bb828c1f", | ||||
|                 "sha256:89ebf69cb19a33d625db72d2ac589d26e936b8f7628531269accf4a3196e7872", | ||||
|                 "sha256:8daecd778c1da45b8bd54fd41ffcd471a86beed3d8e57a43acf7a8d63bba4058", | ||||
|                 "sha256:955ba8ea73cf3ed8bd2f963b4cb9f8f0dcb27becd2f4b3dd536fd24c45533454", | ||||
|                 "sha256:964f18a59f5a64c0170f684c417f4fe3e695a536612e13074c4dd5d1c6d7c882", | ||||
|                 "sha256:969843fbdfbf56cdb71da6f0bdf50f9985b8b8aeb630102945306cf10a9c6af2", | ||||
|                 "sha256:996021ef33e0f50b97ff2d6b5f422a0fe5577de21a8873b58a779a5ddd1c3132", | ||||
|                 "sha256:9e9c9078a7ce07e6fce366bd818be89365a35d2e4b163268f0ca9ba7e13bb2f6", | ||||
|                 "sha256:a04901757cb0fb0f5602ac11dda48f5510f94372144d06c2563ba56c480b467c", | ||||
|                 "sha256:a7bf1492429f18d205f3a818da3ff1f242f60aa59006e53dee00b4ef592a3363", | ||||
|                 "sha256:aa0af2deb166a5e26e0d554b824605e660039b161e37ed4f01b8d04beec184f3", | ||||
|                 "sha256:abfb15a6a7822f0fae681785cb38860e7a2cb1616a708d53df557b3d76c5bfd4", | ||||
|                 "sha256:b253fe4df2afea4dfa6b1fa8c5fef212aff8bcaaeb4207e81eed05cb5e4a7919", | ||||
|                 "sha256:b27f082f47d23cffc4cf1388b84fdc45c4ef6015f906cd7e0d988d9e35d36349", | ||||
|                 "sha256:b33aea449e7f46738811fbc6f0b3177c6777a572207412bbbf6f525ffed001ae", | ||||
|                 "sha256:b44f9421c4505c548435244d74037618f452844c5d3c67719d8a55e2613549da", | ||||
|                 "sha256:bcc371151d1512201d0214c36c0c150b1dc64f19c2b1a8c9cb1d7c7c15ebd93f", | ||||
|                 "sha256:c2851deeabd96d3f6283e9c6b26e0bfed4de2dc6fb15edf913e78b79fc5909ed", | ||||
|                 "sha256:cdfd501c7ac5b198c15df800a3a34c38345f5182e5f80770caf362bccca65628", | ||||
|                 "sha256:d2c0caffa47606d6d7c8af94ba42547bd2a441f06c74fd90a1ffe328524a6c64", | ||||
|                 "sha256:dcb2db95e629962db5a355047fb8aefb012df6c8ae608930d391619dbd96fd86", | ||||
|                 "sha256:e0eeb9c112fec2031927a1745788a181d0eecbacbed941fc5c4f7bc3f7b273bf", | ||||
|                 "sha256:e154891263306200260d7f3051982774d7b9ef35af3509d5adbbe539afd2610c", | ||||
|                 "sha256:e2e023a42dcbab8ed31f97c2bcdb980b7fbe0ada34037d87ba9d799664b58ded", | ||||
|                 "sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390", | ||||
|                 "sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==1.1.0" | ||||
|         }, | ||||
|         "httptools": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be", | ||||
|                 "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d", | ||||
|                 "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce", | ||||
|                 "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2", | ||||
|                 "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6", | ||||
|                 "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f", | ||||
|                 "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009", | ||||
|                 "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce", | ||||
|                 "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a", | ||||
|                 "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c", | ||||
|                 "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4", | ||||
|                 "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437" | ||||
|             ], | ||||
|             "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", | ||||
|             "version": "==0.1.1" | ||||
|         }, | ||||
|         "hyperlink": { | ||||
|             "hashes": [ | ||||
|                 "sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af", | ||||
|                 "sha256:c528d405766f15a2c536230de7e160b65a08e20264d8891b3eb03307b0df3c63" | ||||
|             ], | ||||
|             "version": "==20.0.1" | ||||
|         }, | ||||
|         "idna": { | ||||
|             "hashes": [ | ||||
|                 "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", | ||||
| @ -319,11 +502,19 @@ | ||||
|             ], | ||||
|             "version": "==2.10" | ||||
|         }, | ||||
|         "incremental": { | ||||
|             "hashes": [ | ||||
|                 "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", | ||||
|                 "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3" | ||||
|             ], | ||||
|             "version": "==17.5.0" | ||||
|         }, | ||||
|         "inflection": { | ||||
|             "hashes": [ | ||||
|                 "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", | ||||
|                 "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==0.5.1" | ||||
|         }, | ||||
|         "itypes": { | ||||
| @ -338,6 +529,7 @@ | ||||
|                 "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", | ||||
|                 "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||
|             "version": "==2.11.2" | ||||
|         }, | ||||
|         "jmespath": { | ||||
| @ -345,6 +537,7 @@ | ||||
|                 "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", | ||||
|                 "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==0.10.0" | ||||
|         }, | ||||
|         "jsonschema": { | ||||
| @ -359,11 +552,23 @@ | ||||
|                 "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a", | ||||
|                 "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||
|             "version": "==4.6.11" | ||||
|         }, | ||||
|         "kubernetes": { | ||||
|             "hashes": [ | ||||
|                 "sha256:1a2472f8b01bc6aa87e3a34781f859bded5a5c8ff791a53d889a8bd6cc550430", | ||||
|                 "sha256:4af81201520977139a143f96123fb789fa351879df37f122916b9b6ed050bbaf" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==11.0.0" | ||||
|         }, | ||||
|         "ldap3": { | ||||
|             "hashes": [ | ||||
|                 "sha256:59d1adcd5ead263387039e2a37d7cd772a2006b1cdb3ecfcbaab5192a601c515", | ||||
|                 "sha256:7abbb3e5f4522114e0230ec175b60ae968b938d1f8a7d8bce7789f78d871fb9f", | ||||
|                 "sha256:b399c39e80b6459e349b33fbe9787c1bcbf86de05994d41806a05c06f3e7574d", | ||||
|                 "sha256:bdaf568cd30fc0006c8bb4f5e6014554afeb0c4bbea1677de9706e278a4057e7", | ||||
|                 "sha256:df27407f4991f25bd669b5bb1bc8cb9ddf44a3e713ff6b3afeb3b3c26502f88f" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
| @ -442,13 +647,38 @@ | ||||
|                 "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", | ||||
|                 "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==1.1.1" | ||||
|         }, | ||||
|         "msgpack": { | ||||
|             "hashes": [ | ||||
|                 "sha256:002a0d813e1f7b60da599bdf969e632074f9eec1b96cbed8fb0973a63160a408", | ||||
|                 "sha256:25b3bc3190f3d9d965b818123b7752c5dfb953f0d774b454fd206c18fe384fb8", | ||||
|                 "sha256:271b489499a43af001a2e42f42d876bb98ccaa7e20512ff37ca78c8e12e68f84", | ||||
|                 "sha256:39c54fdebf5fa4dda733369012c59e7d085ebdfe35b6cf648f09d16708f1be5d", | ||||
|                 "sha256:4233b7f86c1208190c78a525cd3828ca1623359ef48f78a6fea4b91bb995775a", | ||||
|                 "sha256:5bea44181fc8e18eed1d0cd76e355073f00ce232ff9653a0ae88cb7d9e643322", | ||||
|                 "sha256:5dba6d074fac9b24f29aaf1d2d032306c27f04187651511257e7831733293ec2", | ||||
|                 "sha256:7a22c965588baeb07242cb561b63f309db27a07382825fc98aecaf0827c1538e", | ||||
|                 "sha256:908944e3f038bca67fcfedb7845c4a257c7749bf9818632586b53bcf06ba4b97", | ||||
|                 "sha256:9534d5cc480d4aff720233411a1f765be90885750b07df772380b34c10ecb5c0", | ||||
|                 "sha256:aa5c057eab4f40ec47ea6f5a9825846be2ff6bf34102c560bad5cad5a677c5be", | ||||
|                 "sha256:b3758dfd3423e358bbb18a7cccd1c74228dffa7a697e5be6cb9535de625c0dbf", | ||||
|                 "sha256:c901e8058dd6653307906c5f157f26ed09eb94a850dddd989621098d347926ab", | ||||
|                 "sha256:cec8bf10981ed70998d98431cd814db0ecf3384e6b113366e7f36af71a0fca08", | ||||
|                 "sha256:db685187a415f51d6b937257474ca72199f393dad89534ebbdd7d7a3b000080e", | ||||
|                 "sha256:e35b051077fc2f3ce12e7c6a34cf309680c63a842db3a0616ea6ed25ad20d272", | ||||
|                 "sha256:e7bbdd8e2b277b77782f3ce34734b0dfde6cbe94ddb74de8d733d603c7f9e2b1", | ||||
|                 "sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140" | ||||
|             ], | ||||
|             "version": "==1.0.0" | ||||
|         }, | ||||
|         "oauthlib": { | ||||
|             "hashes": [ | ||||
|                 "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", | ||||
|                 "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==3.1.0" | ||||
|         }, | ||||
|         "packaging": { | ||||
| @ -504,15 +734,37 @@ | ||||
|         }, | ||||
|         "pyasn1": { | ||||
|             "hashes": [ | ||||
|                 "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", | ||||
|                 "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", | ||||
|                 "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", | ||||
|                 "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", | ||||
|                 "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", | ||||
|                 "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" | ||||
|                 "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", | ||||
|                 "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", | ||||
|                 "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", | ||||
|                 "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", | ||||
|                 "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", | ||||
|                 "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", | ||||
|                 "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", | ||||
|                 "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" | ||||
|             ], | ||||
|             "version": "==0.4.8" | ||||
|         }, | ||||
|         "pyasn1-modules": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", | ||||
|                 "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", | ||||
|                 "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", | ||||
|                 "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", | ||||
|                 "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", | ||||
|                 "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", | ||||
|                 "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" | ||||
|                 "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", | ||||
|                 "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", | ||||
|                 "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", | ||||
|                 "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", | ||||
|                 "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", | ||||
|                 "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", | ||||
|                 "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" | ||||
|             ], | ||||
|             "version": "==0.2.8" | ||||
|         }, | ||||
| @ -521,6 +773,7 @@ | ||||
|                 "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", | ||||
|                 "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==2.20" | ||||
|         }, | ||||
|         "pycryptodome": { | ||||
| @ -592,8 +845,17 @@ | ||||
|                 "sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46", | ||||
|                 "sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==3.9.8" | ||||
|         }, | ||||
|         "pyhamcrest": { | ||||
|             "hashes": [ | ||||
|                 "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", | ||||
|                 "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==2.0.2" | ||||
|         }, | ||||
|         "pyjwkest": { | ||||
|             "hashes": [ | ||||
|                 "sha256:5560fd5ba08655f29ff6ad1df1e15dc05abc9d976fcbcec8d2b5167f49b70222" | ||||
| @ -613,6 +875,7 @@ | ||||
|                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", | ||||
|                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==2.4.7" | ||||
|         }, | ||||
|         "pyrsistent": { | ||||
| @ -626,6 +889,7 @@ | ||||
|                 "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", | ||||
|                 "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==2.8.1" | ||||
|         }, | ||||
|         "pytz": { | ||||
| @ -635,24 +899,6 @@ | ||||
|             ], | ||||
|             "version": "==2020.1" | ||||
|         }, | ||||
|         "pyuwsgi": { | ||||
|             "hashes": [ | ||||
|                 "sha256:1a4dd8d99b8497f109755e09484b0bd2aeaa533f7621e7c7e2a120a72111219d", | ||||
|                 "sha256:206937deaebbac5c87692657c3151a5a9d40ecbc9b051b94154205c50a48e963", | ||||
|                 "sha256:2cf35d9145208cc7c96464d688caa3de745bfc969e1a1ae23cb046fc10b0ac7e", | ||||
|                 "sha256:3ab84a168633eeb55847d59475d86e9078d913d190c2a1aed804c562a10301a3", | ||||
|                 "sha256:430406d1bcf288a87f14fde51c66877eaf5e98516838a1c6f761af5d814936fc", | ||||
|                 "sha256:72be25ce7aa86c5616c59d12c2961b938e7bde47b7ff6a996ff83b89f7c5cd27", | ||||
|                 "sha256:aa4d615de430e2066a1c76d9cc2a70abf2dfc703a82c21aee625b445866f2c3b", | ||||
|                 "sha256:aadd231256a672cf4342ef9fb976051949e4d5b616195e696bcb7b8a9c07789e", | ||||
|                 "sha256:b15ee6a7759b0465786d856334b8231d882deda5291cf243be6a343a8f3ef910", | ||||
|                 "sha256:bd1d0a8d4cb87eb63417a72e6b1bac47053f9b0be550adc6d2a375f4cbaa22f0", | ||||
|                 "sha256:d5787779ec24b67ac8898be9dc2b2b4e35f17d79f14361f6cf303d6283a848f2", | ||||
|                 "sha256:ecfae85d6504e0ecbba100a795032a88ce8f110b62b93243f2df1bd116eca67f" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==2.0.19.1" | ||||
|         }, | ||||
|         "pyyaml": { | ||||
|             "hashes": [ | ||||
|                 "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", | ||||
| @ -683,6 +929,7 @@ | ||||
|                 "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", | ||||
|                 "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||
|             "version": "==3.5.3" | ||||
|         }, | ||||
|         "requests": { | ||||
| @ -690,16 +937,26 @@ | ||||
|                 "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", | ||||
|                 "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||
|             "version": "==2.24.0" | ||||
|         }, | ||||
|         "requests-oauthlib": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", | ||||
|                 "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" | ||||
|                 "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", | ||||
|                 "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.3.0" | ||||
|         }, | ||||
|         "rsa": { | ||||
|             "hashes": [ | ||||
|                 "sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa", | ||||
|                 "sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==4.6" | ||||
|         }, | ||||
|         "ruamel.yaml": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b", | ||||
| @ -729,7 +986,7 @@ | ||||
|                 "sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad", | ||||
|                 "sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e" | ||||
|             ], | ||||
|             "markers": "platform_python_implementation == 'CPython' and python_version < '3.9'", | ||||
|             "markers": "python_version < '3.9' and platform_python_implementation == 'CPython'", | ||||
|             "version": "==0.2.0" | ||||
|         }, | ||||
|         "s3transfer": { | ||||
| @ -741,11 +998,11 @@ | ||||
|         }, | ||||
|         "sentry-sdk": { | ||||
|             "hashes": [ | ||||
|                 "sha256:5b884a391da04696c1d81d636d2ad728fd838370db1acdfda3acbad1fe5be830", | ||||
|                 "sha256:bbfe5633aee4dacb53d79d303ab6bfacf1749fb717750c112fb1658e5accce0d" | ||||
|                 "sha256:0af429c221670e602f960fca85ca3f607c85510a91f11e8be8f742a978127f78", | ||||
|                 "sha256:a088a1054673c6a19ea590045c871c38da029ef743b61a07bfee95e9f3c060f7" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.17.2" | ||||
|             "version": "==0.17.3" | ||||
|         }, | ||||
|         "service-identity": { | ||||
|             "hashes": [ | ||||
| @ -768,6 +1025,7 @@ | ||||
|                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", | ||||
|                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==1.15.0" | ||||
|         }, | ||||
|         "sqlparse": { | ||||
| @ -775,6 +1033,7 @@ | ||||
|                 "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", | ||||
|                 "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==0.3.1" | ||||
|         }, | ||||
|         "structlog": { | ||||
| @ -793,11 +1052,52 @@ | ||||
|             "index": "pypi", | ||||
|             "version": "==2.7.3" | ||||
|         }, | ||||
|         "twisted": { | ||||
|             "extras": [ | ||||
|                 "tls" | ||||
|             ], | ||||
|             "hashes": [ | ||||
|                 "sha256:040eb6641125d2a9a09cf198ec7b83dd8858c6f51f6770325ed9959c00f5098f", | ||||
|                 "sha256:147780b8caf21ba2aef3688628eaf13d7e7fe02a86747cd54bfaf2140538f042", | ||||
|                 "sha256:158ddb80719a4813d292293ac44ba41d8b56555ed009d90994a278237ee63d2c", | ||||
|                 "sha256:2182000d6ffc05d269e6c03bfcec8b57e20259ca1086180edaedec3f1e689292", | ||||
|                 "sha256:25ffcf37944bdad4a99981bc74006d735a678d2b5c193781254fbbb6d69e3b22", | ||||
|                 "sha256:3281d9ce889f7b21bdb73658e887141aa45a102baf3b2320eafcfba954fcefec", | ||||
|                 "sha256:356e8d8dd3590e790e3dba4db139eb8a17aca64b46629c622e1b1597a4a92478", | ||||
|                 "sha256:70952c56e4965b9f53b180daecf20a9595cf22b8d0935cd3bd664c90273c3ab2", | ||||
|                 "sha256:7408c6635ee1b96587289283ebe90ee15dbf9614b05857b446055116bc822d29", | ||||
|                 "sha256:7c547fd0215db9da8a1bc23182b309e84a232364cc26d829e9ee196ce840b114", | ||||
|                 "sha256:894f6f3cfa57a15ea0d0714e4283913a5f2511dbd18653dd148eba53b3919797", | ||||
|                 "sha256:94ac3d55a58c90e2075c5fe1853f2aa3892b73e3bf56395f743aefde8605eeaa", | ||||
|                 "sha256:a58e61a2a01e5bcbe3b575c0099a2bcb8d70a75b1a087338e0c48dd6e01a5f15", | ||||
|                 "sha256:c09c47ff9750a8e3aa60ad169c4b95006d455a29b80ad0901f031a103b2991cd", | ||||
|                 "sha256:ca3a0b8c9110800e576d89b5337373e52018b41069bc879f12fa42b7eb2d0274", | ||||
|                 "sha256:cd1dc5c85b58494138a3917752b54bb1daa0045d234b7c132c37a61d5483ebad", | ||||
|                 "sha256:cdbc4c7f0cd7a2218b575844e970f05a1be1861c607b0e048c9bceca0c4d42f7", | ||||
|                 "sha256:d267125cc0f1e8a0eed6319ba4ac7477da9b78a535601c49ecd20c875576433a", | ||||
|                 "sha256:d72c55b5d56e176563b91d11952d13b01af8725c623e498db5507b6614fc1e10", | ||||
|                 "sha256:d95803193561a243cb0401b0567c6b7987d3f2a67046770e1dccd1c9e49a9780", | ||||
|                 "sha256:e92703bed0cc21d6cb5c61d66922b3b1564015ca8a51325bd164a5e33798d504", | ||||
|                 "sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467", | ||||
|                 "sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||
|             "version": "==20.3.0" | ||||
|         }, | ||||
|         "txaio": { | ||||
|             "hashes": [ | ||||
|                 "sha256:17938f2bca4a9cabce61346758e482ca4e600160cbc28e861493eac74a19539d", | ||||
|                 "sha256:38a469daf93c37e5527cb062653d6393ae11663147c42fab7ddc3f6d00d434ae" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==20.4.1" | ||||
|         }, | ||||
|         "uritemplate": { | ||||
|             "hashes": [ | ||||
|                 "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", | ||||
|                 "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==3.0.1" | ||||
|         }, | ||||
|         "urllib3": { | ||||
| @ -809,15 +1109,119 @@ | ||||
|                 "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "markers": null, | ||||
|             "version": "==1.25.10" | ||||
|         }, | ||||
|         "uvicorn": { | ||||
|             "hashes": [ | ||||
|                 "sha256:46a83e371f37ea7ff29577d00015f02c942410288fb57def6440f2653fff1d26", | ||||
|                 "sha256:4b70ddb4c1946e39db9f3082d53e323dfd50634b95fd83625d778729ef1730ef" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.11.8" | ||||
|         }, | ||||
|         "uvloop": { | ||||
|             "hashes": [ | ||||
|                 "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", | ||||
|                 "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", | ||||
|                 "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", | ||||
|                 "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", | ||||
|                 "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", | ||||
|                 "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", | ||||
|                 "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", | ||||
|                 "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", | ||||
|                 "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" | ||||
|             ], | ||||
|             "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", | ||||
|             "version": "==0.14.0" | ||||
|         }, | ||||
|         "vine": { | ||||
|             "hashes": [ | ||||
|                 "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87", | ||||
|                 "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==1.3.0" | ||||
|         }, | ||||
|         "websocket-client": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549", | ||||
|                 "sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010" | ||||
|             ], | ||||
|             "version": "==0.57.0" | ||||
|         }, | ||||
|         "websockets": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", | ||||
|                 "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", | ||||
|                 "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", | ||||
|                 "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", | ||||
|                 "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", | ||||
|                 "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", | ||||
|                 "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", | ||||
|                 "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", | ||||
|                 "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", | ||||
|                 "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", | ||||
|                 "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", | ||||
|                 "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", | ||||
|                 "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", | ||||
|                 "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", | ||||
|                 "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", | ||||
|                 "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", | ||||
|                 "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", | ||||
|                 "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", | ||||
|                 "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", | ||||
|                 "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", | ||||
|                 "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", | ||||
|                 "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" | ||||
|             ], | ||||
|             "markers": "python_full_version >= '3.6.1'", | ||||
|             "version": "==8.1" | ||||
|         }, | ||||
|         "zope.interface": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0103cba5ed09f27d2e3de7e48bb320338592e2fabc5ce1432cf33808eb2dfd8b", | ||||
|                 "sha256:14415d6979356629f1c386c8c4249b4d0082f2ea7f75871ebad2e29584bd16c5", | ||||
|                 "sha256:1ae4693ccee94c6e0c88a4568fb3b34af8871c60f5ba30cf9f94977ed0e53ddd", | ||||
|                 "sha256:1b87ed2dc05cb835138f6a6e3595593fea3564d712cb2eb2de963a41fd35758c", | ||||
|                 "sha256:269b27f60bcf45438e8683269f8ecd1235fa13e5411de93dae3b9ee4fe7f7bc7", | ||||
|                 "sha256:27d287e61639d692563d9dab76bafe071fbeb26818dd6a32a0022f3f7ca884b5", | ||||
|                 "sha256:39106649c3082972106f930766ae23d1464a73b7d30b3698c986f74bf1256a34", | ||||
|                 "sha256:40e4c42bd27ed3c11b2c983fecfb03356fae1209de10686d03c02c8696a1d90e", | ||||
|                 "sha256:461d4339b3b8f3335d7e2c90ce335eb275488c587b61aca4b305196dde2ff086", | ||||
|                 "sha256:4f98f70328bc788c86a6a1a8a14b0ea979f81ae6015dd6c72978f1feff70ecda", | ||||
|                 "sha256:558a20a0845d1a5dc6ff87cd0f63d7dac982d7c3be05d2ffb6322a87c17fa286", | ||||
|                 "sha256:562dccd37acec149458c1791da459f130c6cf8902c94c93b8d47c6337b9fb826", | ||||
|                 "sha256:5e86c66a6dea8ab6152e83b0facc856dc4d435fe0f872f01d66ce0a2131b7f1d", | ||||
|                 "sha256:60a207efcd8c11d6bbeb7862e33418fba4e4ad79846d88d160d7231fcb42a5ee", | ||||
|                 "sha256:645a7092b77fdbc3f68d3cc98f9d3e71510e419f54019d6e282328c0dd140dcd", | ||||
|                 "sha256:6874367586c020705a44eecdad5d6b587c64b892e34305bb6ed87c9bbe22a5e9", | ||||
|                 "sha256:74bf0a4f9091131de09286f9a605db449840e313753949fe07c8d0fe7659ad1e", | ||||
|                 "sha256:7b726194f938791a6691c7592c8b9e805fc6d1b9632a833b9c0640828cd49cbc", | ||||
|                 "sha256:8149ded7f90154fdc1a40e0c8975df58041a6f693b8f7edcd9348484e9dc17fe", | ||||
|                 "sha256:8cccf7057c7d19064a9e27660f5aec4e5c4001ffcf653a47531bde19b5aa2a8a", | ||||
|                 "sha256:911714b08b63d155f9c948da2b5534b223a1a4fc50bb67139ab68b277c938578", | ||||
|                 "sha256:a5f8f85986197d1dd6444763c4a15c991bfed86d835a1f6f7d476f7198d5f56a", | ||||
|                 "sha256:a744132d0abaa854d1aad50ba9bc64e79c6f835b3e92521db4235a1991176813", | ||||
|                 "sha256:af2c14efc0bb0e91af63d00080ccc067866fb8cbbaca2b0438ab4105f5e0f08d", | ||||
|                 "sha256:b054eb0a8aa712c8e9030065a59b5e6a5cf0746ecdb5f087cca5ec7685690c19", | ||||
|                 "sha256:b0becb75418f8a130e9d465e718316cd17c7a8acce6fe8fe07adc72762bee425", | ||||
|                 "sha256:b1d2ed1cbda2ae107283befd9284e650d840f8f7568cb9060b5466d25dc48975", | ||||
|                 "sha256:ba4261c8ad00b49d48bbb3b5af388bb7576edfc0ca50a49c11dcb77caa1d897e", | ||||
|                 "sha256:d1fe9d7d09bb07228650903d6a9dc48ea649e3b8c69b1d263419cc722b3938e8", | ||||
|                 "sha256:d7804f6a71fc2dda888ef2de266727ec2f3915373d5a785ed4ddc603bbc91e08", | ||||
|                 "sha256:da2844fba024dd58eaa712561da47dcd1e7ad544a257482392472eae1c86d5e5", | ||||
|                 "sha256:dcefc97d1daf8d55199420e9162ab584ed0893a109f45e438b9794ced44c9fd0", | ||||
|                 "sha256:dd98c436a1fc56f48c70882cc243df89ad036210d871c7427dc164b31500dc11", | ||||
|                 "sha256:e74671e43ed4569fbd7989e5eecc7d06dc134b571872ab1d5a88f4a123814e9f", | ||||
|                 "sha256:eb9b92f456ff3ec746cd4935b73c1117538d6124b8617bc0fe6fda0b3816e345", | ||||
|                 "sha256:ebb4e637a1fb861c34e48a00d03cffa9234f42bef923aec44e5625ffb9a8e8f9", | ||||
|                 "sha256:ef739fe89e7f43fb6494a43b1878a36273e5924869ba1d866f752c5812ae8d58", | ||||
|                 "sha256:f40db0e02a8157d2b90857c24d89b6310f9b6c3642369852cdc3b5ac49b92afc", | ||||
|                 "sha256:f68bf937f113b88c866d090fea0bc52a098695173fc613b055a17ff0cf9683b6", | ||||
|                 "sha256:fb55c182a3f7b84c1a2d6de5fa7b1a05d4660d866b91dbf8d74549c57a1499e8" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||
|             "version": "==5.1.0" | ||||
|         } | ||||
|     }, | ||||
|     "develop": { | ||||
| @ -833,6 +1237,7 @@ | ||||
|                 "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", | ||||
|                 "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==3.2.10" | ||||
|         }, | ||||
|         "astroid": { | ||||
| @ -840,6 +1245,7 @@ | ||||
|                 "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", | ||||
|                 "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==2.4.1" | ||||
|         }, | ||||
|         "attrs": { | ||||
| @ -847,6 +1253,7 @@ | ||||
|                 "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", | ||||
|                 "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==20.1.0" | ||||
|         }, | ||||
|         "autopep8": { | ||||
| @ -877,6 +1284,7 @@ | ||||
|                 "sha256:477f0e18a0d58e50bb3dbc9af7fcda464fd0ebfc7a6151d8888602d7153171a0", | ||||
|                 "sha256:cd4f3a231305e405ed8944d8ff35bd742d9bc740ad62f483bd0ca21ce7131984" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==1.0.0" | ||||
|         }, | ||||
|         "bumpversion": { | ||||
| @ -906,6 +1314,7 @@ | ||||
|                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", | ||||
|                 "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||
|             "version": "==7.1.2" | ||||
|         }, | ||||
|         "colorama": { | ||||
| @ -992,6 +1401,7 @@ | ||||
|                 "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", | ||||
|                 "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==3.8.3" | ||||
|         }, | ||||
|         "flake8-polyfill": { | ||||
| @ -1006,6 +1416,7 @@ | ||||
|                 "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", | ||||
|                 "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.4'", | ||||
|             "version": "==4.0.5" | ||||
|         }, | ||||
|         "gitpython": { | ||||
| @ -1013,6 +1424,7 @@ | ||||
|                 "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858", | ||||
|                 "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.4'", | ||||
|             "version": "==3.1.7" | ||||
|         }, | ||||
|         "idna": { | ||||
| @ -1027,6 +1439,7 @@ | ||||
|                 "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", | ||||
|                 "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==4.3.21" | ||||
|         }, | ||||
|         "lazy-object-proxy": { | ||||
| @ -1053,6 +1466,7 @@ | ||||
|                 "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", | ||||
|                 "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==1.4.3" | ||||
|         }, | ||||
|         "mccabe": { | ||||
| @ -1074,6 +1488,7 @@ | ||||
|                 "sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea", | ||||
|                 "sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.6'", | ||||
|             "version": "==5.5.0" | ||||
|         }, | ||||
|         "pep8-naming": { | ||||
| @ -1095,6 +1510,7 @@ | ||||
|                 "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", | ||||
|                 "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==2.6.0" | ||||
|         }, | ||||
|         "pydocstyle": { | ||||
| @ -1102,6 +1518,7 @@ | ||||
|                 "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", | ||||
|                 "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==5.1.1" | ||||
|         }, | ||||
|         "pyflakes": { | ||||
| @ -1109,6 +1526,7 @@ | ||||
|                 "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", | ||||
|                 "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==2.2.0" | ||||
|         }, | ||||
|         "pylint": { | ||||
| @ -1201,6 +1619,7 @@ | ||||
|                 "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", | ||||
|                 "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||
|             "version": "==2.24.0" | ||||
|         }, | ||||
|         "requirements-detector": { | ||||
| @ -1228,6 +1647,7 @@ | ||||
|                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", | ||||
|                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==1.15.0" | ||||
|         }, | ||||
|         "smmap": { | ||||
| @ -1235,6 +1655,7 @@ | ||||
|                 "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", | ||||
|                 "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==3.0.4" | ||||
|         }, | ||||
|         "snowballstemmer": { | ||||
| @ -1249,6 +1670,7 @@ | ||||
|                 "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", | ||||
|                 "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==0.3.1" | ||||
|         }, | ||||
|         "stevedore": { | ||||
| @ -1256,6 +1678,7 @@ | ||||
|                 "sha256:a34086819e2c7a7f86d5635363632829dab8014e5fd7be2454c7cba84ac7514e", | ||||
|                 "sha256:ddc09a744dc224c84ec8e8efcb70595042d21c97c76df60daee64c9ad53bc7ee" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.6'", | ||||
|             "version": "==3.2.1" | ||||
|         }, | ||||
|         "toml": { | ||||
| @ -1308,7 +1731,6 @@ | ||||
|                 "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "markers": null, | ||||
|             "version": "==1.25.10" | ||||
|         }, | ||||
|         "websocket-client": { | ||||
|  | ||||
| @ -4,7 +4,6 @@ | ||||
|  | ||||
| [](https://codecov.io/gh/BeryJu/passbook) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -261,18 +261,6 @@ stages: | ||||
|             command: 'buildAndPush' | ||||
|             Dockerfile: 'Dockerfile' | ||||
|             tags: 'gh-$(Build.SourceBranchName)' | ||||
|       - job: build_gatekeeper | ||||
|         pool: | ||||
|           vmImage: 'ubuntu-latest' | ||||
|         steps: | ||||
|         - task: Docker@2 | ||||
|           inputs: | ||||
|             containerRegistry: 'dockerhub' | ||||
|             repository: 'beryju/passbook-gatekeeper' | ||||
|             command: 'buildAndPush' | ||||
|             Dockerfile: 'gatekeeper/Dockerfile' | ||||
|             buildContext: 'gatekeeper/' | ||||
|             tags: 'gh-$(Build.SourceBranchName)' | ||||
|       - job: build_static | ||||
|         pool: | ||||
|           vmImage: 'ubuntu-latest' | ||||
|  | ||||
| @ -22,14 +22,13 @@ services: | ||||
|       - traefik.enable=false | ||||
|   server: | ||||
|     image: beryju/passbook:${PASSBOOK_TAG:-latest} | ||||
|     command: | ||||
|       - uwsgi | ||||
|       - uwsgi.ini | ||||
|     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_REDIS__HOST: redis | ||||
|       PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false} | ||||
|       PASSBOOK_POSTGRESQL__HOST: postgresql | ||||
|       PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword} | ||||
|       PASSBOOK_LOG_LEVEL: debug | ||||
|     ports: | ||||
|       - 8000 | ||||
|     networks: | ||||
| @ -40,23 +39,17 @@ services: | ||||
|       - traefik.frontend.rule=PathPrefix:/ | ||||
|   worker: | ||||
|     image: beryju/passbook:${PASSBOOK_TAG:-latest} | ||||
|     command: | ||||
|       - celery | ||||
|       - worker | ||||
|       - --autoscale=10,3 | ||||
|       - -E | ||||
|       - -B | ||||
|       - -A=passbook.root.celery | ||||
|       - -s=/tmp/celerybeat-schedule | ||||
|     command: worker | ||||
|     networks: | ||||
|       - internal | ||||
|     labels: | ||||
|       - traefik.enable=false | ||||
|     environment: | ||||
|       - PASSBOOK_REDIS__HOST=redis | ||||
|       - PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false} | ||||
|       - PASSBOOK_POSTGRESQL__HOST=postgresql | ||||
|       - PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword} | ||||
|       PASSBOOK_REDIS__HOST: redis | ||||
|       PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false} | ||||
|       PASSBOOK_POSTGRESQL__HOST: postgresql | ||||
|       PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword} | ||||
|       PASSBOOK_LOG_LEVEL: debug | ||||
|   static: | ||||
|     image: beryju/passbook-static:latest | ||||
|     networks: | ||||
|  | ||||
| @ -1,3 +1,10 @@ | ||||
| #!/bin/bash -ex | ||||
| #!/bin/bash -e | ||||
| /app/wait_for_db.py | ||||
| "$@" | ||||
| printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@" | ||||
| if [[ "$1" == "server" ]]; then | ||||
|     gunicorn -c 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 | ||||
| else | ||||
|     ./manage.py "$@" | ||||
| fi | ||||
|  | ||||
							
								
								
									
										38
									
								
								docker/gunicorn.conf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								docker/gunicorn.conf.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| """Gunicorn config""" | ||||
| import multiprocessing | ||||
|  | ||||
| import structlog | ||||
|  | ||||
| bind = "0.0.0.0:8000" | ||||
| workers = multiprocessing.cpu_count() * 2 + 1 | ||||
| workers = 1 | ||||
|  | ||||
| user = "passbook" | ||||
| group = "passbook" | ||||
|  | ||||
| worker_class = "uvicorn.workers.UvicornWorker" | ||||
|  | ||||
| logconfig_dict = { | ||||
|     "version": 1, | ||||
|     "disable_existing_loggers": False, | ||||
|     "formatters": { | ||||
|         "json_formatter": { | ||||
|             "()": structlog.stdlib.ProcessorFormatter, | ||||
|             "processor": structlog.processors.JSONRenderer(), | ||||
|             "foreign_pre_chain": [ | ||||
|                 structlog.stdlib.add_log_level, | ||||
|                 structlog.stdlib.add_logger_name, | ||||
|                 structlog.processors.TimeStamper(), | ||||
|                 structlog.processors.StackInfoRenderer(), | ||||
|                 structlog.processors.format_exc_info, | ||||
|             ], | ||||
|         } | ||||
|     }, | ||||
|     "handlers": { | ||||
|         "error_console": { | ||||
|             "class": "logging.StreamHandler", | ||||
|             "formatter": "json_formatter", | ||||
|         }, | ||||
|         "console": {"class": "logging.StreamHandler", "formatter": "json_formatter"}, | ||||
|     }, | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| [uwsgi] | ||||
| http = 0.0.0.0:8000 | ||||
| wsgi-file = passbook/root/wsgi.py | ||||
| processes = 2 | ||||
| master = true | ||||
| threads = 2 | ||||
| enable-threads = true | ||||
| uid = passbook | ||||
| gid = passbook | ||||
| disable-logging = True | ||||
| @ -6,3 +6,4 @@ services: | ||||
|     volumes: | ||||
|       - /dev/shm:/dev/shm | ||||
|     network_mode: host | ||||
|     restart: always | ||||
|  | ||||
| @ -1,8 +0,0 @@ | ||||
| FROM quay.io/oauth2-proxy/oauth2-proxy | ||||
|  | ||||
| ENV OAUTH2_PROXY_EMAIL_DOMAINS=* | ||||
| ENV OAUTH2_PROXY_PROVIDER=oidc | ||||
| ENV OAUTH2_PROXY_HTTP_ADDRESS=:4180 | ||||
| # TODO: If service is access over HTTPS, this needs to be set to true (default), otherwise needs to be false | ||||
| # ENV OAUTH2_PROXY_COOKIE_SECURE=true | ||||
| ENV OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true | ||||
| @ -53,9 +53,7 @@ spec: | ||||
|         - name: {{ .Chart.Name }} | ||||
|           image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" | ||||
|           imagePullPolicy: Always | ||||
|           args: | ||||
|             - uwsgi | ||||
|             - uwsgi.ini | ||||
|           args: server | ||||
|           envFrom: | ||||
|             - configMapRef: | ||||
|                 name: {{ include "passbook.fullname" . }}-config | ||||
|  | ||||
| @ -26,14 +26,7 @@ spec: | ||||
|         - name: {{ .Chart.Name }} | ||||
|           image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" | ||||
|           imagePullPolicy: IfNotPresent | ||||
|           args: | ||||
|             - celery | ||||
|             - worker | ||||
|             - --autoscale=10,3 | ||||
|             - -E | ||||
|             - -B | ||||
|             - -A=passbook.root.celery | ||||
|             - -s=/tmp/celerybeat-schedule | ||||
|           args: worker | ||||
|           envFrom: | ||||
|             - configMapRef: | ||||
|                 name: "{{ include "passbook.fullname" . }}-config" | ||||
|  | ||||
| @ -46,6 +46,12 @@ | ||||
|                         {% trans 'Providers' %} | ||||
|                     </a> | ||||
|                 </li> | ||||
|                 <li class="pf-c-nav__item"> | ||||
|                     <a href="{% url 'passbook_admin:outposts' %}" | ||||
|                         class="pf-c-nav__link {% is_active 'passbook_admin:outposts' 'passbook_admin:outpost-create' 'passbook_admin:outpost-update' 'passbook_admin:outpost-delete' %}"> | ||||
|                         {% trans 'Outposts' %} | ||||
|                     </a> | ||||
|                 </li> | ||||
|                 <li class="pf-c-nav__item"> | ||||
|                     <a href="{% url 'passbook_admin:property-mappings' %}" | ||||
|                         class="pf-c-nav__link {% is_active 'passbook_admin:property-mappings' 'passbook_admin:property-mapping-create' 'passbook_admin:property-mapping-update' 'passbook_admin:property-mapping-delete' %}"> | ||||
|  | ||||
							
								
								
									
										96
									
								
								passbook/admin/templates/administration/outpost/list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								passbook/admin/templates/administration/outpost/list.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,96 @@ | ||||
| {% extends "administration/base.html" %} | ||||
|  | ||||
| {% load i18n %} | ||||
| {% load humanize %} | ||||
| {% load passbook_utils %} | ||||
|  | ||||
| {% block head %} | ||||
| {{ block.super }} | ||||
| <style> | ||||
| .pf-m-success { | ||||
|     color: var(--pf-global--success-color--100); | ||||
| } | ||||
| .pf-m-danger { | ||||
|     color: var(--pf-global--danger-color--100); | ||||
| } | ||||
| </style> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
| <section class="pf-c-page__main-section pf-m-light"> | ||||
|     <div class="pf-c-content"> | ||||
|         <h1> | ||||
|             <i class="fas fa-map-marker"></i> | ||||
|             {% trans 'Outposts' %} | ||||
|         </h1> | ||||
|         <p>{% trans "Outposts are deployments of passbook components to support different environments and protocols, like reverse proxies." %}</p> | ||||
|     </div> | ||||
| </section> | ||||
| <section class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|     <div class="pf-c-card"> | ||||
|         {% if object_list %} | ||||
|         <div class="pf-c-toolbar"> | ||||
|             <div class="pf-c-toolbar__content"> | ||||
|                 <div class="pf-c-toolbar__bulk-select"> | ||||
|                     <a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||
|                 </div> | ||||
|                 {% include 'partials/pagination.html' %} | ||||
|             </div> | ||||
|         </div> | ||||
|         <table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid"> | ||||
|             <thead> | ||||
|                 <tr role="row"> | ||||
|                     <th role="columnheader" scope="col">{% trans 'Name' %}</th> | ||||
|                     <th role="columnheader" scope="col">{% trans 'Providers' %}</th> | ||||
|                     <th role="columnheader" scope="col">{% trans 'Health' %}</th> | ||||
|                     <th role="cell"></th> | ||||
|                 </tr> | ||||
|             </thead> | ||||
|             <tbody role="rowgroup"> | ||||
|                 {% for outpost in object_list %} | ||||
|                 <tr role="row"> | ||||
|                     <th role="columnheader"> | ||||
|                         <a href="{% url 'passbook_outposts:setup' outpost_pk=outpost.pk %}">{{ outpost.name }}</a> | ||||
|                     </th> | ||||
|                     <td role="cell"> | ||||
|                         <span> | ||||
|                             {{ outpost.providers.all.select_subclasses|join:", " }} | ||||
|                         </span> | ||||
|                     </td> | ||||
|                     <td role="cell"> | ||||
|                         {% with health=outpost.health %} | ||||
|                         {% if health %} | ||||
|                             <i class="fas fa-check pf-m-success"></i> {{ health|naturaltime }} | ||||
|                         {% else %} | ||||
|                             <i class="fas fa-times pf-m-danger"></i> Unhealthy | ||||
|                         {% endif %} | ||||
|                         {% endwith %} | ||||
|                     </td> | ||||
|                     <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> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|                 {% endfor %} | ||||
|             </tbody> | ||||
|         </table> | ||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> | ||||
|             {% include 'partials/pagination.html' %} | ||||
|         </div> | ||||
|         {% else %} | ||||
|         <div class="pf-c-empty-state"> | ||||
|             <div class="pf-c-empty-state__content"> | ||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> | ||||
|                 <h1 class="pf-c-title pf-m-lg"> | ||||
|                     {% trans 'No Outposts.' %} | ||||
|                 </h1> | ||||
|                 <div class="pf-c-empty-state__body"> | ||||
|                     {% trans 'Currently no outposts exist. Click the button below to create one.' %} | ||||
|                 </div> | ||||
|                 <a href="{% url 'passbook_admin:outpost-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||
|             </div> | ||||
|         </div> | ||||
|         {% endif %} | ||||
|     </div> | ||||
| </section> | ||||
| {% endblock %} | ||||
| @ -36,7 +36,7 @@ | ||||
|                 <tr role="row"> | ||||
|                     <th role="columnheader"> | ||||
|                         <div> | ||||
|                             <div>{{ token.pk }}</div> | ||||
|                             <div>{{ token.pk.hex }}</div> | ||||
|                         </div> | ||||
|                     </th> | ||||
|                     <td role="cell"> | ||||
| @ -51,7 +51,11 @@ | ||||
|                     </td> | ||||
|                     <td role="cell"> | ||||
|                         <span> | ||||
|                             {% if not token.expiring %} | ||||
|                             - | ||||
|                             {% else %} | ||||
|                             {{ token.expires }} | ||||
|                             {% endif %} | ||||
|                         </span> | ||||
|                     </td> | ||||
|                     <td> | ||||
|  | ||||
| @ -6,6 +6,7 @@ from passbook.admin.views import ( | ||||
|     certificate_key_pair, | ||||
|     flows, | ||||
|     groups, | ||||
|     outposts, | ||||
|     overview, | ||||
|     policies, | ||||
|     policies_bindings, | ||||
| @ -271,4 +272,19 @@ urlpatterns = [ | ||||
|         certificate_key_pair.CertificateKeyPairDeleteView.as_view(), | ||||
|         name="certificatekeypair-delete", | ||||
|     ), | ||||
|     # Outposts | ||||
|     path("outposts/", outposts.OutpostListView.as_view(), name="outposts",), | ||||
|     path( | ||||
|         "outposts/create/", outposts.OutpostCreateView.as_view(), name="outpost-create", | ||||
|     ), | ||||
|     path( | ||||
|         "outposts/<uuid:pk>/update/", | ||||
|         outposts.OutpostUpdateView.as_view(), | ||||
|         name="outpost-update", | ||||
|     ), | ||||
|     path( | ||||
|         "outposts/<uuid:pk>/delete/", | ||||
|         outposts.OutpostDeleteView.as_view(), | ||||
|         name="outpost-delete", | ||||
|     ), | ||||
| ] | ||||
|  | ||||
							
								
								
									
										67
									
								
								passbook/admin/views/outposts.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								passbook/admin/views/outposts.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,67 @@ | ||||
| """passbook Outpost administration""" | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.contrib.auth.mixins import ( | ||||
|     PermissionRequiredMixin as DjangoPermissionRequiredMixin, | ||||
| ) | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.views.generic import ListView, UpdateView | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
|  | ||||
| from passbook.admin.views.utils import DeleteMessageView | ||||
| from passbook.lib.views import CreateAssignPermView | ||||
| from passbook.outposts.forms import OutpostForm | ||||
| from passbook.outposts.models import Outpost | ||||
|  | ||||
|  | ||||
| class OutpostListView(LoginRequiredMixin, PermissionListMixin, ListView): | ||||
|     """Show list of all outposts""" | ||||
|  | ||||
|     model = Outpost | ||||
|     permission_required = "passbook_outposts.view_outpost" | ||||
|     ordering = "name" | ||||
|     paginate_by = 40 | ||||
|     template_name = "administration/outpost/list.html" | ||||
|  | ||||
|  | ||||
| class OutpostCreateView( | ||||
|     SuccessMessageMixin, | ||||
|     LoginRequiredMixin, | ||||
|     DjangoPermissionRequiredMixin, | ||||
|     CreateAssignPermView, | ||||
| ): | ||||
|     """Create new Outpost""" | ||||
|  | ||||
|     model = Outpost | ||||
|     form_class = OutpostForm | ||||
|     permission_required = "passbook_outposts.add_outpost" | ||||
|  | ||||
|     template_name = "generic/create.html" | ||||
|     success_url = reverse_lazy("passbook_admin:outposts") | ||||
|     success_message = _("Successfully created Outpost") | ||||
|  | ||||
|  | ||||
| class OutpostUpdateView( | ||||
|     SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView | ||||
| ): | ||||
|     """Update outpost""" | ||||
|  | ||||
|     model = Outpost | ||||
|     form_class = OutpostForm | ||||
|     permission_required = "passbook_outposts.change_outpost" | ||||
|  | ||||
|     template_name = "generic/update.html" | ||||
|     success_url = reverse_lazy("passbook_admin:outposts") | ||||
|     success_message = _("Successfully updated Certificate-Key Pair") | ||||
|  | ||||
|  | ||||
| class OutpostDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView): | ||||
|     """Delete outpost""" | ||||
|  | ||||
|     model = Outpost | ||||
|     permission_required = "passbook_outposts.delete_outpost" | ||||
|  | ||||
|     template_name = "generic/delete.html" | ||||
|     success_url = reverse_lazy("passbook_admin:outposts") | ||||
|     success_message = _("Successfully deleted Certificate-Key Pair") | ||||
| @ -6,15 +6,17 @@ from drf_yasg.views import get_schema_view | ||||
| from rest_framework import routers | ||||
|  | ||||
| from passbook.api.permissions import CustomObjectPermissions | ||||
| from passbook.api.v2.messages import MessagesViewSet | ||||
| from passbook.audit.api import EventViewSet | ||||
| from passbook.core.api.applications import ApplicationViewSet | ||||
| from passbook.core.api.groups import GroupViewSet | ||||
| from passbook.core.api.messages import MessagesViewSet | ||||
| from passbook.core.api.propertymappings import PropertyMappingViewSet | ||||
| from passbook.core.api.providers import ProviderViewSet | ||||
| from passbook.core.api.sources import SourceViewSet | ||||
| from passbook.core.api.users import UserViewSet | ||||
| from passbook.crypto.api import CertificateKeyPairViewSet | ||||
| from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet | ||||
| from passbook.outposts.api import OutpostViewSet | ||||
| from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet | ||||
| from passbook.policies.dummy.api import DummyPolicyViewSet | ||||
| from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet | ||||
| @ -24,7 +26,7 @@ from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet | ||||
| from passbook.policies.password.api import PasswordPolicyViewSet | ||||
| from passbook.policies.reputation.api import ReputationPolicyViewSet | ||||
| from passbook.providers.oauth2.api import OAuth2ProviderViewSet, ScopeMappingViewSet | ||||
| from passbook.providers.proxy.api import ProxyProviderViewSet | ||||
| from passbook.providers.proxy.api import OutpostConfigViewSet, ProxyProviderViewSet | ||||
| from passbook.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet | ||||
| from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet | ||||
| from passbook.sources.oauth.api import OAuthSourceViewSet | ||||
| @ -47,10 +49,14 @@ from passbook.stages.user_write.api import UserWriteStageViewSet | ||||
|  | ||||
| router = routers.DefaultRouter() | ||||
|  | ||||
| router.register("root/messages", MessagesViewSet, basename="messages") | ||||
| router.register("core/applications", ApplicationViewSet) | ||||
| router.register("core/groups", GroupViewSet) | ||||
| router.register("core/users", UserViewSet) | ||||
| router.register("core/messages", MessagesViewSet, basename="messages") | ||||
| router.register("outposts/outposts", OutpostViewSet) | ||||
| router.register("outposts/proxy", OutpostConfigViewSet) | ||||
|  | ||||
| router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet) | ||||
|  | ||||
| router.register("audit/events", EventViewSet) | ||||
|  | ||||
|  | ||||
| @ -9,10 +9,11 @@ from passbook.lib.widgets import GroupedModelChoiceField | ||||
| class ApplicationForm(forms.ModelForm): | ||||
|     """Application Form""" | ||||
|  | ||||
|     provider = GroupedModelChoiceField( | ||||
|         queryset=Provider.objects.all().order_by("pk").select_subclasses(), | ||||
|         required=False, | ||||
|     ) | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.fields["provider"].queryset = ( | ||||
|             Provider.objects.all().order_by("pk").select_subclasses() | ||||
|         ) | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
| @ -32,6 +33,7 @@ class ApplicationForm(forms.ModelForm): | ||||
|             "meta_icon_url": forms.TextInput(), | ||||
|             "meta_publisher": forms.TextInput(), | ||||
|         } | ||||
|         field_classes = {"provider": GroupedModelChoiceField} | ||||
|         labels = { | ||||
|             "meta_launch_url": _("Launch URL"), | ||||
|             "meta_icon_url": _("Icon URL"), | ||||
|  | ||||
							
								
								
									
										36
									
								
								passbook/core/migrations/0008_auto_20200824_1532.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								passbook/core/migrations/0008_auto_20200824_1532.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| # Generated by Django 3.1 on 2020-08-24 15:32 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("auth", "0012_alter_user_first_name_max_length"), | ||||
|         ("passbook_core", "0007_auto_20200815_1841"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RemoveField( | ||||
|             model_name="user", | ||||
|             name="groups", | ||||
|             field=models.ManyToManyField(to="passbook_core.Group"), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="user", | ||||
|             name="groups", | ||||
|             field=models.ManyToManyField( | ||||
|                 blank=True, | ||||
|                 help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", | ||||
|                 related_name="user_set", | ||||
|                 related_query_name="user", | ||||
|                 to="auth.Group", | ||||
|                 verbose_name="groups", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="user", | ||||
|             name="pb_groups", | ||||
|             field=models.ManyToManyField(to="passbook_core.Group"), | ||||
|         ), | ||||
|     ] | ||||
| @ -58,7 +58,7 @@ class User(GuardianUserMixin, AbstractUser): | ||||
|     name = models.TextField(help_text=_("User's display name.")) | ||||
|  | ||||
|     sources = models.ManyToManyField("Source", through="UserSourceConnection") | ||||
|     groups = models.ManyToManyField("Group") | ||||
|     pb_groups = models.ManyToManyField("Group") | ||||
|     password_change_date = models.DateTimeField(auto_now_add=True) | ||||
|  | ||||
|     attributes = models.JSONField(default=dict, blank=True) | ||||
|  | ||||
| @ -8,6 +8,10 @@ | ||||
| {% trans card_title %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block card_title %} | ||||
| {% trans card_title %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block card %} | ||||
| <form method="POST" class="pf-c-form"> | ||||
|     {% if message %} | ||||
|  | ||||
| @ -29,7 +29,7 @@ | ||||
|         <main class="pf-c-login__main"> | ||||
|             <header class="pf-c-login__main-header"> | ||||
|                 <h1 class="pf-c-title pf-m-3xl"> | ||||
|                     {% block title %} | ||||
|                     {% block card_title %} | ||||
|                     {% endblock %} | ||||
|                 </h1> | ||||
|             </header> | ||||
|  | ||||
| @ -4,6 +4,10 @@ | ||||
| {% load i18n %} | ||||
| {% load passbook_utils %} | ||||
|  | ||||
| {% block card_title %} | ||||
| {% trans 'Permission denied' %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block title %} | ||||
| {% trans 'Permission denied' %} | ||||
| {% endblock %} | ||||
|  | ||||
							
								
								
									
										47
									
								
								passbook/crypto/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								passbook/crypto/api.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| """Crypto API Views""" | ||||
| from cryptography.hazmat.backends import default_backend | ||||
| from cryptography.hazmat.primitives.serialization import load_pem_private_key | ||||
| from cryptography.x509 import load_pem_x509_certificate | ||||
| from rest_framework.serializers import ModelSerializer, ValidationError | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from passbook.crypto.models import CertificateKeyPair | ||||
|  | ||||
|  | ||||
| class CertificateKeyPairSerializer(ModelSerializer): | ||||
|     """CertificateKeyPair Serializer""" | ||||
|  | ||||
|     def validate_certificate_data(self, value): | ||||
|         """Verify that input is a valid PEM x509 Certificate""" | ||||
|         try: | ||||
|             load_pem_x509_certificate(value.encode("utf-8"), default_backend()) | ||||
|         except ValueError: | ||||
|             raise ValidationError("Unable to load certificate.") | ||||
|         return value | ||||
|  | ||||
|     def validate_key_data(self, value): | ||||
|         """Verify that input is a valid PEM RSA Key""" | ||||
|         # Since this field is optional, data can be empty. | ||||
|         if value == "": | ||||
|             return value | ||||
|         try: | ||||
|             load_pem_private_key( | ||||
|                 str.encode("\n".join([x.strip() for x in value.split("\n")])), | ||||
|                 password=None, | ||||
|                 backend=default_backend(), | ||||
|             ) | ||||
|         except ValueError: | ||||
|             raise ValidationError("Unable to load private key.") | ||||
|         return value | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = CertificateKeyPair | ||||
|         fields = ["pk", "name", "certificate_data", "key_data"] | ||||
|  | ||||
|  | ||||
| class CertificateKeyPairViewSet(ModelViewSet): | ||||
|     """CertificateKeyPair Viewset""" | ||||
|  | ||||
|     queryset = CertificateKeyPair.objects.all() | ||||
|     serializer_class = CertificateKeyPairSerializer | ||||
| @ -13,5 +13,4 @@ class PassbookFlowsConfig(AppConfig): | ||||
|     verbose_name = "passbook Flows" | ||||
|  | ||||
|     def ready(self): | ||||
|         """Flow signals that clear the cache""" | ||||
|         import_module("passbook.flows.signals") | ||||
|  | ||||
							
								
								
									
										18
									
								
								passbook/flows/migrations/0012_auto_20200830_1056.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								passbook/flows/migrations/0012_auto_20200830_1056.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.1 on 2020-08-30 10:56 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("passbook_flows", "0011_flow_title"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="flow", | ||||
|             name="title", | ||||
|             field=models.TextField(blank=True, default=""), | ||||
|         ), | ||||
|     ] | ||||
| @ -3,18 +3,17 @@ import os | ||||
| from collections.abc import Mapping | ||||
| from contextlib import contextmanager | ||||
| from glob import glob | ||||
| from json import dumps | ||||
| from typing import Any, Dict | ||||
| from urllib.parse import urlparse | ||||
|  | ||||
| import yaml | ||||
| from django.conf import ImproperlyConfigured | ||||
| from django.http import HttpRequest | ||||
| from structlog import get_logger | ||||
|  | ||||
| SEARCH_PATHS = ["passbook/lib/default.yml", "/etc/passbook/config.yml", ""] + glob( | ||||
|     "/etc/passbook/config.d/*.yml", recursive=True | ||||
| ) | ||||
| LOGGER = get_logger() | ||||
| ENV_PREFIX = "PASSBOOK" | ||||
| ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local") | ||||
|  | ||||
| @ -58,6 +57,13 @@ class ConfigLoader: | ||||
|                         self.update_from_file(env_file) | ||||
|         self.update_from_env() | ||||
|  | ||||
|     def _log(self, level: str, message: str, **kwargs): | ||||
|         """Custom Log method, we want to ensure ConfigLoader always logs JSON even when | ||||
|         'structlog' or 'logging' hasn't been configured yet.""" | ||||
|         output = {"event": message, "level": level, "logger": self.__class__.__module__} | ||||
|         output.update(kwargs) | ||||
|         print(dumps(output)) | ||||
|  | ||||
|     def update(self, root, updatee): | ||||
|         """Recursively update dictionary""" | ||||
|         for key, value in updatee.items(): | ||||
| @ -82,12 +88,14 @@ class ConfigLoader: | ||||
|             with open(path) as file: | ||||
|                 try: | ||||
|                     self.update(self.__config, yaml.safe_load(file)) | ||||
|                     LOGGER.debug("Loaded config", file=path) | ||||
|                     self._log("debug", "Loaded config", file=path) | ||||
|                     self.loaded_file.append(path) | ||||
|                 except yaml.YAMLError as exc: | ||||
|                     raise ImproperlyConfigured from exc | ||||
|         except PermissionError as exc: | ||||
|             LOGGER.warning("Permission denied while reading file", path=path, error=exc) | ||||
|             self._log( | ||||
|                 "warning", "Permission denied while reading file", path=path, error=exc | ||||
|             ) | ||||
|  | ||||
|     def update_from_dict(self, update: dict): | ||||
|         """Update config from dict""" | ||||
| @ -111,7 +119,7 @@ class ConfigLoader: | ||||
|             current_obj[dot_parts[-1]] = value | ||||
|             idx += 1 | ||||
|         if idx > 0: | ||||
|             LOGGER.debug("Loaded environment variables", count=idx) | ||||
|             self._log("debug", "Loaded environment variables", count=idx) | ||||
|             self.update(self.__config, outer) | ||||
|  | ||||
|     @contextmanager | ||||
|  | ||||
| @ -12,7 +12,7 @@ redis: | ||||
|   message_queue_db: 1 | ||||
|  | ||||
| debug: false | ||||
| log_level: warning | ||||
| log_level: info | ||||
|  | ||||
| # Error reporting, sends stacktrace to sentry.beryju.org | ||||
| error_reporting: | ||||
|  | ||||
							
								
								
									
										0
									
								
								passbook/outposts/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/outposts/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										23
									
								
								passbook/outposts/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								passbook/outposts/api.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| """Outpost API Views""" | ||||
| from rest_framework.serializers import JSONField, ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from passbook.outposts.models import Outpost | ||||
|  | ||||
|  | ||||
| class OutpostSerializer(ModelSerializer): | ||||
|     """Outpost Serializer""" | ||||
|  | ||||
|     _config = JSONField() | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = Outpost | ||||
|         fields = ["pk", "name", "providers", "_config"] | ||||
|  | ||||
|  | ||||
| class OutpostViewSet(ModelViewSet): | ||||
|     """Outpost Viewset""" | ||||
|  | ||||
|     queryset = Outpost.objects.all() | ||||
|     serializer_class = OutpostSerializer | ||||
							
								
								
									
										16
									
								
								passbook/outposts/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								passbook/outposts/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| """passbook outposts app config""" | ||||
| from importlib import import_module | ||||
|  | ||||
| from django.apps import AppConfig | ||||
|  | ||||
|  | ||||
| class PassbookOutpostConfig(AppConfig): | ||||
|     """passbook outposts app config""" | ||||
|  | ||||
|     name = "passbook.outposts" | ||||
|     label = "passbook_outposts" | ||||
|     mountpoint = "outposts/" | ||||
|     verbose_name = "passbook Outpost" | ||||
|  | ||||
|     def ready(self): | ||||
|         import_module("passbook.outposts.signals") | ||||
							
								
								
									
										100
									
								
								passbook/outposts/channels.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								passbook/outposts/channels.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | ||||
| """Outpost websocket handler""" | ||||
| from dataclasses import asdict, dataclass, field | ||||
| from enum import IntEnum | ||||
| from time import time | ||||
| from typing import Any, Dict | ||||
|  | ||||
| from channels.generic.websocket import JsonWebsocketConsumer | ||||
| from dacite import from_dict | ||||
| from dacite.data import Data | ||||
| from django.core.cache import cache | ||||
| from django.core.exceptions import ValidationError | ||||
| from structlog import get_logger | ||||
|  | ||||
| from passbook.core.models import Token, TokenIntents | ||||
| from passbook.outposts.models import Outpost | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| class WebsocketMessageInstruction(IntEnum): | ||||
|     """Commands which can be triggered over Websocket""" | ||||
|  | ||||
|     # Simple message used by either side when a message is acknowledged | ||||
|     ACK = 0 | ||||
|  | ||||
|     # Message used by outposts to report their alive status | ||||
|     HELLO = 1 | ||||
|  | ||||
|     # Message sent by us to trigger an Update | ||||
|     TRIGGER_UPDATE = 2 | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class WebsocketMessage: | ||||
|     """Complete Websocket Message that is being sent""" | ||||
|  | ||||
|     instruction: int | ||||
|     args: Dict[str, Any] = field(default_factory=dict) | ||||
|  | ||||
|  | ||||
| class OutpostConsumer(JsonWebsocketConsumer): | ||||
|     """Handler for Outposts that connect over websockets for health checks and live updates""" | ||||
|  | ||||
|     outpost: Outpost | ||||
|  | ||||
|     def connect(self): | ||||
|         # TODO: This authentication block could be handeled in middleware | ||||
|         headers = dict(self.scope["headers"]) | ||||
|         if b"authorization" not in headers: | ||||
|             LOGGER.warning("WS Request without authorization header") | ||||
|             self.close() | ||||
|  | ||||
|         token = headers[b"authorization"] | ||||
|         try: | ||||
|             token_uuid = token.decode("utf-8") | ||||
|             tokens = Token.filter_not_expired( | ||||
|                 token_uuid=token_uuid, intent=TokenIntents.INTENT_API | ||||
|             ) | ||||
|             if not tokens.exists(): | ||||
|                 LOGGER.warning("WS Request with invalid token") | ||||
|                 self.close() | ||||
|         except ValidationError: | ||||
|             LOGGER.warning("WS Invalid UUID") | ||||
|             self.close() | ||||
|  | ||||
|         uuid = self.scope["url_route"]["kwargs"]["pk"] | ||||
|         outpost = Outpost.objects.filter(pk=uuid) | ||||
|         if not outpost.exists(): | ||||
|             self.close() | ||||
|             return | ||||
|         self.accept() | ||||
|         self.outpost = outpost.first() | ||||
|         self.outpost.channels.append(self.channel_name) | ||||
|         LOGGER.debug("added channel to outpost", channel_name=self.channel_name) | ||||
|         self.outpost.save() | ||||
|  | ||||
|     # pylint: disable=unused-argument | ||||
|     def disconnect(self, close_code): | ||||
|         self.outpost.channels.remove(self.channel_name) | ||||
|         self.outpost.save() | ||||
|         LOGGER.debug("removed channel from outpost", channel_name=self.channel_name) | ||||
|  | ||||
|     def receive_json(self, content: Data): | ||||
|         msg = from_dict(WebsocketMessage, content) | ||||
|         if msg.instruction == WebsocketMessageInstruction.HELLO: | ||||
|             cache.set(self.outpost.health_cache_key, time(), timeout=60) | ||||
|         elif msg.instruction == WebsocketMessageInstruction.ACK: | ||||
|             return | ||||
|  | ||||
|         response = WebsocketMessage(instruction=WebsocketMessageInstruction.ACK) | ||||
|         self.send_json(asdict(response)) | ||||
|  | ||||
|     # pylint: disable=unused-argument | ||||
|     def event_update(self, event): | ||||
|         """Event handler which is called by post_save signals""" | ||||
|         self.send_json( | ||||
|             asdict( | ||||
|                 WebsocketMessage(instruction=WebsocketMessageInstruction.TRIGGER_UPDATE) | ||||
|             ) | ||||
|         ) | ||||
							
								
								
									
										0
									
								
								passbook/outposts/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/outposts/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										29
									
								
								passbook/outposts/controllers/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								passbook/outposts/controllers/base.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| """Base Controller""" | ||||
| from typing import Dict | ||||
|  | ||||
| from structlog import get_logger | ||||
|  | ||||
| from passbook.outposts.models import Outpost | ||||
|  | ||||
|  | ||||
| class BaseController: | ||||
|     """Base Outpost deployment controller""" | ||||
|  | ||||
|     deployment_ports: Dict[str, int] | ||||
|  | ||||
|     outpost: Outpost | ||||
|  | ||||
|     def __init__(self, outpost_pk: str): | ||||
|         self.outpost = Outpost.objects.get(pk=outpost_pk) | ||||
|         self.logger = get_logger( | ||||
|             controller=self.__class__.__name__, outpost=self.outpost | ||||
|         ) | ||||
|         self.deployment_ports = {} | ||||
|  | ||||
|     def run(self): | ||||
|         """Called by scheduled task to reconcile deployment/service/etc""" | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def get_static_deployment(self) -> str: | ||||
|         """Return a static deployment configuration""" | ||||
|         raise NotImplementedError | ||||
							
								
								
									
										36
									
								
								passbook/outposts/controllers/compose.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								passbook/outposts/controllers/compose.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| """Docker Compose controller""" | ||||
| from yaml import safe_dump | ||||
|  | ||||
| from passbook import __version__ | ||||
| from passbook.outposts.controllers.base import BaseController | ||||
|  | ||||
|  | ||||
| class DockerComposeController(BaseController): | ||||
|     """Docker Compose controller""" | ||||
|  | ||||
|     image_base = "beryju/passbook" | ||||
|  | ||||
|     def run(self): | ||||
|         self.logger.warning("DockerComposeController does not implement run") | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def get_static_deployment(self) -> str: | ||||
|         """Generate docker-compose yaml for proxy, version 3.5""" | ||||
|         ports = [f"{x}:{x}" for _, x in self.deployment_ports.items()] | ||||
|         compose = { | ||||
|             "version": "3.5", | ||||
|             "services": { | ||||
|                 f"passbook_{self.outpost.type}": { | ||||
|                     "image": f"{self.image_base}-{self.outpost.type}:{__version__}", | ||||
|                     "ports": ports, | ||||
|                     "environment": { | ||||
|                         "PASSBOOK_HOST": self.outpost.config.passbook_host, | ||||
|                         "PASSBOOK_INSECURE": str( | ||||
|                             self.outpost.config.passbook_host_insecure | ||||
|                         ), | ||||
|                         "PASSBOOK_TOKEN": self.outpost.token.token_uuid.hex, | ||||
|                     }, | ||||
|                 } | ||||
|             }, | ||||
|         } | ||||
|         return safe_dump(compose, default_flow_style=False) | ||||
							
								
								
									
										143
									
								
								passbook/outposts/controllers/kubernetes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								passbook/outposts/controllers/kubernetes.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,143 @@ | ||||
| """Kubernetes deployment controller""" | ||||
| from io import StringIO | ||||
|  | ||||
| from kubernetes.client import ( | ||||
|     V1Container, | ||||
|     V1ContainerPort, | ||||
|     V1Deployment, | ||||
|     V1DeploymentSpec, | ||||
|     V1EnvVar, | ||||
|     V1EnvVarSource, | ||||
|     V1LabelSelector, | ||||
|     V1ObjectMeta, | ||||
|     V1PodSpec, | ||||
|     V1PodTemplateSpec, | ||||
|     V1Secret, | ||||
|     V1SecretKeySelector, | ||||
|     V1Service, | ||||
|     V1ServicePort, | ||||
|     V1ServiceSpec, | ||||
| ) | ||||
| from yaml import dump_all | ||||
|  | ||||
| from passbook import __version__ | ||||
| from passbook.outposts.controllers.base import BaseController | ||||
|  | ||||
|  | ||||
| class KubernetesController(BaseController): | ||||
|     """Manage deployment of outpost in kubernetes""" | ||||
|  | ||||
|     image_base = "beryju/passbook" | ||||
|  | ||||
|     def run(self): | ||||
|         """Called by scheduled task to reconcile deployment/service/etc""" | ||||
|         # TODO | ||||
|  | ||||
|     def get_static_deployment(self) -> str: | ||||
|         with StringIO() as _str: | ||||
|             dump_all( | ||||
|                 [ | ||||
|                     self.get_deployment_secret(), | ||||
|                     self.get_deployment(), | ||||
|                     self.get_service(), | ||||
|                 ], | ||||
|                 stream=_str, | ||||
|                 default_flow_style=False, | ||||
|             ) | ||||
|             return _str.getvalue() | ||||
|  | ||||
|     def get_object_meta(self, **kwargs) -> V1ObjectMeta: | ||||
|         """Get common object metadata""" | ||||
|         return V1ObjectMeta( | ||||
|             namespace="self.instance.namespace", | ||||
|             labels={ | ||||
|                 "app.kubernetes.io/name": f"passbook-{self.outpost.type.lower()}", | ||||
|                 "app.kubernetes.io/instance": self.outpost.name, | ||||
|                 "app.kubernetes.io/version": __version__, | ||||
|                 "app.kubernetes.io/managed-by": "passbook.beryju.org", | ||||
|                 "passbook.beryju.org/outpost/uuid": self.outpost.uuid.hex, | ||||
|             }, | ||||
|             **kwargs, | ||||
|         ) | ||||
|  | ||||
|     def get_deployment_secret(self) -> V1Secret: | ||||
|         """Get secret with token and passbook host""" | ||||
|         return V1Secret( | ||||
|             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 | ||||
|                 ), | ||||
|                 "token": self.outpost.token.token_uuid.hex, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     def get_service(self) -> V1Service: | ||||
|         """Get service object for outpost based on ports defined""" | ||||
|         meta = self.get_object_meta(name=f"passbook-outpost-{self.outpost.name}") | ||||
|         ports = [] | ||||
|         for port_name, port in self.deployment_ports.items(): | ||||
|             ports.append(V1ServicePort(name=port_name, port=port)) | ||||
|         return V1Service( | ||||
|             metadata=meta, | ||||
|             spec=V1ServiceSpec(ports=ports, selector=meta.labels, type="ClusterIP"), | ||||
|         ) | ||||
|  | ||||
|     def get_deployment(self) -> V1Deployment: | ||||
|         """Get deployment object for outpost""" | ||||
|         # Generate V1ContainerPort objects | ||||
|         container_ports = [] | ||||
|         for port_name, port in self.deployment_ports.items(): | ||||
|             container_ports.append(V1ContainerPort(container_port=port, name=port_name)) | ||||
|         meta = self.get_object_meta(name=f"passbook-outpost-{self.outpost.name}") | ||||
|         return V1Deployment( | ||||
|             metadata=meta, | ||||
|             spec=V1DeploymentSpec( | ||||
|                 replicas=1, | ||||
|                 selector=V1LabelSelector(match_labels=meta.labels), | ||||
|                 template=V1PodTemplateSpec( | ||||
|                     metadata=V1ObjectMeta(labels=meta.labels), | ||||
|                     spec=V1PodSpec( | ||||
|                         containers=[ | ||||
|                             V1Container( | ||||
|                                 name=self.outpost.type, | ||||
|                                 image=f"{self.image_base}-{self.outpost.type}:{__version__}", | ||||
|                                 ports=container_ports, | ||||
|                                 env=[ | ||||
|                                     V1EnvVar( | ||||
|                                         name="PASSBOOK_HOST", | ||||
|                                         value_from=V1EnvVarSource( | ||||
|                                             secret_key_ref=V1SecretKeySelector( | ||||
|                                                 name=f"passbook-outpost-{self.outpost.name}-api", | ||||
|                                                 key="passbook_host", | ||||
|                                             ) | ||||
|                                         ), | ||||
|                                     ), | ||||
|                                     V1EnvVar( | ||||
|                                         name="PASSBOOK_TOKEN", | ||||
|                                         value_from=V1EnvVarSource( | ||||
|                                             secret_key_ref=V1SecretKeySelector( | ||||
|                                                 name=f"passbook-outpost-{self.outpost.name}-api", | ||||
|                                                 key="token", | ||||
|                                             ) | ||||
|                                         ), | ||||
|                                     ), | ||||
|                                     V1EnvVar( | ||||
|                                         name="PASSBOOK_INSECURE", | ||||
|                                         value_from=V1EnvVarSource( | ||||
|                                             secret_key_ref=V1SecretKeySelector( | ||||
|                                                 name=f"passbook-outpost-{self.outpost.name}-api", | ||||
|                                                 key="passbook_host_insecure", | ||||
|                                             ) | ||||
|                                         ), | ||||
|                                     ), | ||||
|                                 ], | ||||
|                             ) | ||||
|                         ] | ||||
|                     ), | ||||
|                 ), | ||||
|             ), | ||||
|         ) | ||||
							
								
								
									
										35
									
								
								passbook/outposts/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								passbook/outposts/forms.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| """Outpost forms""" | ||||
|  | ||||
| from django import forms | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| from passbook.admin.fields import CodeMirrorWidget, YAMLField | ||||
| from passbook.core.models import Provider | ||||
| from passbook.outposts.models import Outpost | ||||
|  | ||||
|  | ||||
| class OutpostForm(forms.ModelForm): | ||||
|     """Outpost Form""" | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.fields["providers"].queryset = Provider.objects.all().select_subclasses() | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = Outpost | ||||
|         fields = [ | ||||
|             "name", | ||||
|             "type", | ||||
|             "deployment_type", | ||||
|             "providers", | ||||
|             "_config", | ||||
|         ] | ||||
|         widgets = { | ||||
|             "name": forms.TextInput(), | ||||
|             "_config": CodeMirrorWidget, | ||||
|         } | ||||
|         field_classes = { | ||||
|             "_config": YAMLField, | ||||
|         } | ||||
|         labels = {"_config": _("Configuration")} | ||||
							
								
								
									
										40
									
								
								passbook/outposts/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								passbook/outposts/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| # Generated by Django 3.1 on 2020-08-25 20:45 | ||||
|  | ||||
| import uuid | ||||
|  | ||||
| import django.contrib.postgres.fields | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     initial = True | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("passbook_core", "0008_auto_20200824_1532"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name="Outpost", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "uuid", | ||||
|                     models.UUIDField( | ||||
|                         default=uuid.uuid4, | ||||
|                         editable=False, | ||||
|                         primary_key=True, | ||||
|                         serialize=False, | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("name", models.TextField()), | ||||
|                 ( | ||||
|                     "channels", | ||||
|                     django.contrib.postgres.fields.ArrayField( | ||||
|                         base_field=models.TextField(), size=None | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("providers", models.ManyToManyField(to="passbook_core.Provider")), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										27
									
								
								passbook/outposts/migrations/0002_auto_20200826_1306.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								passbook/outposts/migrations/0002_auto_20200826_1306.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| # Generated by Django 3.1 on 2020-08-26 13:06 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
| import passbook.outposts.models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("passbook_outposts", "0001_initial"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="outpost", | ||||
|             name="_config", | ||||
|             field=models.JSONField( | ||||
|                 default=passbook.outposts.models.default_outpost_config | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="outpost", | ||||
|             name="type", | ||||
|             field=models.TextField(choices=[("proxy", "Proxy")], default="proxy"), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										34
									
								
								passbook/outposts/migrations/0003_auto_20200827_2108.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								passbook/outposts/migrations/0003_auto_20200827_2108.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | ||||
| # Generated by Django 3.1 on 2020-08-27 21:08 | ||||
|  | ||||
| import django.contrib.postgres.fields | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("passbook_outposts", "0002_auto_20200826_1306"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="outpost", | ||||
|             name="deployment_type", | ||||
|             field=models.TextField( | ||||
|                 choices=[ | ||||
|                     ("docker_compose", "Docker Compose"), | ||||
|                     ("kubernetes", "Kubernetes"), | ||||
|                     ("custom", "Custom"), | ||||
|                 ], | ||||
|                 default="custom", | ||||
|                 help_text="Select between passbook-managed deployment types or a custom deployment.", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="outpost", | ||||
|             name="channels", | ||||
|             field=django.contrib.postgres.fields.ArrayField( | ||||
|                 base_field=models.TextField(), default=list, size=None | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										22
									
								
								passbook/outposts/migrations/0004_auto_20200830_1056.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								passbook/outposts/migrations/0004_auto_20200830_1056.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| # Generated by Django 3.1 on 2020-08-30 10:56 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("passbook_outposts", "0003_auto_20200827_2108"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="outpost", | ||||
|             name="deployment_type", | ||||
|             field=models.TextField( | ||||
|                 choices=[("kubernetes", "Kubernetes"), ("custom", "Custom")], | ||||
|                 default="custom", | ||||
|                 help_text="Select between passbook-managed deployment types or a custom deployment.", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										0
									
								
								passbook/outposts/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/outposts/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										148
									
								
								passbook/outposts/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								passbook/outposts/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,148 @@ | ||||
| """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.utils.translation import gettext_lazy as _ | ||||
| from guardian.shortcuts import assign_perm | ||||
|  | ||||
| from passbook.core.models import Provider, Token, TokenIntents, User | ||||
| from passbook.lib.config import CONFIG | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class OutpostConfig: | ||||
|     """Configuration an outpost uses to configure it self""" | ||||
|  | ||||
|     passbook_host: str | ||||
|     passbook_host_insecure: bool = False | ||||
|  | ||||
|     log_level: str = CONFIG.y("log_level") | ||||
|     error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled") | ||||
|     error_reporting_environment: str = CONFIG.y( | ||||
|         "error_reporting.environment", "customer" | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class OutpostModel: | ||||
|     """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 OutpostType(models.TextChoices): | ||||
|     """Outpost types, currently only the reverse proxy is available""" | ||||
|  | ||||
|     PROXY = "proxy" | ||||
|  | ||||
|  | ||||
| class OutpostDeploymentType(models.TextChoices): | ||||
|     """Deployment types that are managed through passbook""" | ||||
|  | ||||
|     KUBERNETES = "kubernetes" | ||||
|     CUSTOM = "custom" | ||||
|  | ||||
|  | ||||
| def default_outpost_config(): | ||||
|     """Get default outpost config""" | ||||
|     return asdict(OutpostConfig(passbook_host="")) | ||||
|  | ||||
|  | ||||
| class Outpost(models.Model): | ||||
|     """Outpost instance which manages a service user and token""" | ||||
|  | ||||
|     uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True) | ||||
|     name = models.TextField() | ||||
|  | ||||
|     type = models.TextField(choices=OutpostType.choices, default=OutpostType.PROXY) | ||||
|     deployment_type = models.TextField( | ||||
|         choices=OutpostDeploymentType.choices, | ||||
|         default=OutpostDeploymentType.CUSTOM, | ||||
|         help_text=_( | ||||
|             "Select between passbook-managed deployment types or a custom deployment." | ||||
|         ), | ||||
|     ) | ||||
|     _config = models.JSONField(default=default_outpost_config) | ||||
|  | ||||
|     providers = models.ManyToManyField(Provider) | ||||
|  | ||||
|     channels = ArrayField(models.TextField(), default=list) | ||||
|  | ||||
|     @property | ||||
|     def config(self) -> OutpostConfig: | ||||
|         """Load config as OutpostConfig object""" | ||||
|         return from_dict(OutpostConfig, loads(self._config)) | ||||
|  | ||||
|     @config.setter | ||||
|     def config(self, value): | ||||
|         """Dump config into json""" | ||||
|         self._config = dumps(asdict(value)) | ||||
|  | ||||
|     @property | ||||
|     def health_cache_key(self) -> str: | ||||
|         """Key by which the outposts health status is saved""" | ||||
|         return f"outpost_{self.uuid.hex}_health" | ||||
|  | ||||
|     @property | ||||
|     def health(self) -> Optional[datetime]: | ||||
|         """Get outpost's health status""" | ||||
|         key = self.health_cache_key | ||||
|         value = cache.get(key, None) | ||||
|         if value: | ||||
|             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() | ||||
|  | ||||
|     @property | ||||
|     def token(self) -> Token: | ||||
|         """Get/create token for auto-generated user""" | ||||
|         token = Token.filter_not_expired(user=self.user, intent=TokenIntents.INTENT_API) | ||||
|         if token.exists(): | ||||
|             return token.first() | ||||
|         return Token.objects.create( | ||||
|             user=self.user, | ||||
|             intent=TokenIntents.INTENT_API, | ||||
|             description=f"Autogenerated by passbook for Outpost {self.name}", | ||||
|             expiring=False, | ||||
|         ) | ||||
|  | ||||
|     def get_required_objects(self) -> Iterable[models.Model]: | ||||
|         """Get an iterator of all objects the user needs read access to""" | ||||
|         objects = [self] | ||||
|         for provider in ( | ||||
|             Provider.objects.filter(outpost=self).select_related().select_subclasses() | ||||
|         ): | ||||
|             if isinstance(provider, OutpostModel): | ||||
|                 objects.extend(provider.get_required_objects()) | ||||
|             else: | ||||
|                 objects.append(provider) | ||||
|         return objects | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return f"Outpost {self.name}" | ||||
							
								
								
									
										10
									
								
								passbook/outposts/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								passbook/outposts/settings.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| """Outposts Settings""" | ||||
| 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"}, | ||||
|     } | ||||
| } | ||||
							
								
								
									
										58
									
								
								passbook/outposts/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								passbook/outposts/signals.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| """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.outposts.models import Outpost, OutpostModel | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @receiver(post_save, sender=Outpost) | ||||
| # pylint: disable=unused-argument | ||||
| def ensure_user_and_token(sender, instance, **_): | ||||
|     """Ensure that token is created/updated on save""" | ||||
|     _ = instance.token | ||||
|  | ||||
|  | ||||
| @receiver(post_save) | ||||
| # pylint: disable=unused-argument | ||||
| def post_save_update(sender, instance, **_): | ||||
|     """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) | ||||
|         return | ||||
|  | ||||
|     for field in instance._meta.get_fields(): | ||||
|         # Each field is checked if it has a `related_model` attribute (when ForeginKeys or M2Ms) | ||||
|         # are used, and if it has a value | ||||
|         if not hasattr(field, "related_model"): | ||||
|             continue | ||||
|         if not field.related_model: | ||||
|             continue | ||||
|         if not issubclass(field.related_model, OutpostModel): | ||||
|             continue | ||||
|  | ||||
|         field_name = f"{field.name}_set" | ||||
|         if not hasattr(instance, field_name): | ||||
|             continue | ||||
|  | ||||
|         LOGGER.debug("triggering outpost update from from field", field=field.name) | ||||
|         # 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: | ||||
|             print(f"sending update to channel {channel}") | ||||
|             async_to_sync(channel_layer.send)(channel, {"type": "event.update"}) | ||||
							
								
								
									
										22
									
								
								passbook/outposts/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								passbook/outposts/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| """outpost tasks""" | ||||
| from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType | ||||
| from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController | ||||
| from passbook.root.celery import CELERY_APP | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True) | ||||
| # pylint: disable=unused-argument | ||||
| def outpost_k8s_controller(self): | ||||
|     """Launch Kubernetes Controller for all Outposts which are deployed in kubernetes""" | ||||
|     for outpost in Outpost.objects.filter( | ||||
|         deployment_type=OutpostDeploymentType.KUBERNETES | ||||
|     ): | ||||
|         outpost_k8s_controller_single.delay(outpost.pk.hex, outpost.type) | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True) | ||||
| # pylint: disable=unused-argument | ||||
| 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() | ||||
| @ -45,8 +45,8 @@ | ||||
|                 <h1 class="pf-c-title pf-m-2xl">{% trans 'Setup with Kubernetes' %}</h1> | ||||
|             </div> | ||||
|             <div class="pf-c-modal-box__body"> | ||||
|                 <p>{% trans 'Download the manifest to create the Gatekeeper deployment and service:' %}</p> | ||||
|                 <a href="{% url 'passbook_providers_app_gw:k8s-manifest' provider=provider.pk %}">{% trans 'Here' %}</a> | ||||
|                 <p>{% trans 'Download the manifest to create the Proxy deployment and service:' %}</p> | ||||
|                 <a href="{% url 'passbook_providers_proxy:k8s-manifest' provider=provider.pk %}">{% trans 'Here' %}</a> | ||||
|                 <p>{% trans 'Afterwards, add the following annotations to the Ingress you want to secure:' %}</p> | ||||
|                 <textarea class="codemirror" readonly data-cm-mode="yaml"> | ||||
| nginx.ingress.kubernetes.io/auth-signin: https://$host/oauth2/start?rd=$escaped_request_uri | ||||
							
								
								
									
										59
									
								
								passbook/outposts/templates/outposts/setup_dc.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								passbook/outposts/templates/outposts/setup_dc.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| {% extends "administration/base.html" %} | ||||
|  | ||||
| {% load i18n %} | ||||
| {% load humanize %} | ||||
| {% load passbook_utils %} | ||||
|  | ||||
| {% block head %} | ||||
| {{ block.super }} | ||||
| <style> | ||||
| .pf-m-success { | ||||
|     color: var(--pf-global--success-color--100); | ||||
| } | ||||
| .pf-m-danger { | ||||
|     color: var(--pf-global--danger-color--100); | ||||
| } | ||||
| </style> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
| <section class="pf-c-page__main-section pf-m-light"> | ||||
|     <div class="pf-c-content"> | ||||
|         <h1> | ||||
|             <i class="fas fa-map-marker"></i> | ||||
|             {% trans 'Outpost Setup' %} | ||||
|         </h1> | ||||
|         <p>{% trans "Outposts are deployments of passbook components to support different environments and protocols, like reverse proxies." %}</p> | ||||
|     </div> | ||||
| </section> | ||||
| <div class="pf-c-tabs pf-m-fill" id="filled-example"> | ||||
|     <button class="pf-c-tabs__scroll-button" disabled aria-hidden="true" aria-label="Scroll left"> | ||||
|         <i class="fas fa-angle-left" aria-hidden="true"></i> | ||||
|     </button> | ||||
|     <ul class="pf-c-tabs__list"> | ||||
|         <li class="pf-c-tabs__item"> | ||||
|             <button class="pf-c-tabs__link" id="filled-example-users-link"> | ||||
|                 <span class="pf-c-tabs__item-text">Users</span> | ||||
|             </button> | ||||
|         </li> | ||||
|         <li class="pf-c-tabs__item pf-m-current"> | ||||
|             <button class="pf-c-tabs__link" id="filled-example-containers-link"> | ||||
|                 <span class="pf-c-tabs__item-text">Containers</span> | ||||
|             </button> | ||||
|         </li> | ||||
|         <li class="pf-c-tabs__item"> | ||||
|             <button class="pf-c-tabs__link" id="filled-example-database-link"> | ||||
|                 <span class="pf-c-tabs__item-text">Database</span> | ||||
|             </button> | ||||
|         </li> | ||||
|     </ul> | ||||
|     <button class="pf-c-tabs__scroll-button" disabled aria-hidden="true" aria-label="Scroll right"> | ||||
|         <i class="fas fa-angle-right" aria-hidden="true"></i> | ||||
|     </button> | ||||
| </div> | ||||
| <section class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|     <div class="pf-c-card"> | ||||
|  | ||||
|     </div> | ||||
| </section> | ||||
| {% endblock %} | ||||
| @ -0,0 +1,59 @@ | ||||
| {% extends "administration/base.html" %} | ||||
|  | ||||
| {% load i18n %} | ||||
| {% load humanize %} | ||||
| {% load passbook_utils %} | ||||
|  | ||||
| {% block head %} | ||||
| {{ block.super }} | ||||
| <style> | ||||
| .pf-m-success { | ||||
|     color: var(--pf-global--success-color--100); | ||||
| } | ||||
| .pf-m-danger { | ||||
|     color: var(--pf-global--danger-color--100); | ||||
| } | ||||
| </style> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
| <section class="pf-c-page__main-section pf-m-light"> | ||||
|     <div class="pf-c-content"> | ||||
|         <h1> | ||||
|             <i class="fas fa-map-marker"></i> | ||||
|             {% trans 'Outpost Setup' %} | ||||
|         </h1> | ||||
|         <p>{% trans "Outposts are deployments of passbook components to support different environments and protocols, like reverse proxies." %}</p> | ||||
|     </div> | ||||
| </section> | ||||
| <div class="pf-c-tabs pf-m-fill" id="filled-example"> | ||||
|     <button class="pf-c-tabs__scroll-button" disabled aria-hidden="true" aria-label="Scroll left"> | ||||
|         <i class="fas fa-angle-left" aria-hidden="true"></i> | ||||
|     </button> | ||||
|     <ul class="pf-c-tabs__list"> | ||||
|         <li class="pf-c-tabs__item"> | ||||
|             <button class="pf-c-tabs__link" id="filled-example-users-link"> | ||||
|                 <span class="pf-c-tabs__item-text">Users</span> | ||||
|             </button> | ||||
|         </li> | ||||
|         <li class="pf-c-tabs__item pf-m-current"> | ||||
|             <button class="pf-c-tabs__link" id="filled-example-containers-link"> | ||||
|                 <span class="pf-c-tabs__item-text">Containers</span> | ||||
|             </button> | ||||
|         </li> | ||||
|         <li class="pf-c-tabs__item"> | ||||
|             <button class="pf-c-tabs__link" id="filled-example-database-link"> | ||||
|                 <span class="pf-c-tabs__item-text">Database</span> | ||||
|             </button> | ||||
|         </li> | ||||
|     </ul> | ||||
|     <button class="pf-c-tabs__scroll-button" disabled aria-hidden="true" aria-label="Scroll right"> | ||||
|         <i class="fas fa-angle-right" aria-hidden="true"></i> | ||||
|     </button> | ||||
| </div> | ||||
| <section class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|     <div class="pf-c-card"> | ||||
|  | ||||
|     </div> | ||||
| </section> | ||||
| {% endblock %} | ||||
							
								
								
									
										96
									
								
								passbook/outposts/templates/outposts/setup_k8s_manual.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								passbook/outposts/templates/outposts/setup_k8s_manual.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,96 @@ | ||||
| {% extends "administration/base.html" %} | ||||
|  | ||||
| {% load i18n %} | ||||
| {% load humanize %} | ||||
| {% load passbook_utils %} | ||||
|  | ||||
| {% block content %} | ||||
| <section class="pf-c-page__main-section pf-m-light"> | ||||
|     <div class="pf-c-content"> | ||||
|         <h1> | ||||
|             <i class="fas fa-map-marker"></i> | ||||
|             {% trans 'Outpost Setup' %} | ||||
|         </h1> | ||||
|         <p>{% trans "Outposts are deployments of passbook components to support different environments and protocols, like reverse proxies." %}</p> | ||||
|     </div> | ||||
| </section> | ||||
| <section class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|     <div class="pf-c-card"> | ||||
|         <pre>apiVersion: apps/v1 | ||||
| kind: Deployment | ||||
| metadata: | ||||
|   labels: | ||||
|     app.kubernetes.io/name: "passbook-{{ outpost.type }}" | ||||
|     app.kubernetes.io/instance: "{{ outpost.name }}" | ||||
|     passbook.beryju.org/outpost: "{{ outpost.pk.hex }}" | ||||
|   name: "passbook-{{ outpost.type }}-{{ outpost.name }}" | ||||
| spec: | ||||
|   replicas: 1 | ||||
|   selector: | ||||
|     matchLabels: | ||||
|       app.kubernetes.io/name: "passbook-{{ outpost.type }}" | ||||
|       app.kubernetes.io/instance: "{{ outpost.name }}" | ||||
|       passbook.beryju.org/outpost: "{{ outpost.pk.hex }}" | ||||
|   template: | ||||
|     metadata: | ||||
|       labels: | ||||
|         app.kubernetes.io/name: "passbook-{{ outpost.type }}" | ||||
|         app.kubernetes.io/instance: "{{ outpost.name }}" | ||||
|         passbook.beryju.org/outpost: "{{ outpost.pk.hex }}" | ||||
|     spec: | ||||
|       containers: | ||||
|       - env: | ||||
|         - name: PASSBOOK_HOST | ||||
|           value: "{{ host }}" | ||||
|         - name: PASSBOOK_TOKEN | ||||
|           value: "{{ outpost.token.pk.hex }}" | ||||
|         image: beryju/passbook-{{ outpost.type }}:{{ version }} | ||||
|         name: "passbook-{{ outpost.type }}" | ||||
|         ports: | ||||
|         - containerPort: 4180 | ||||
|           protocol: TCP | ||||
|           name: http | ||||
|         - containerPort: 4443 | ||||
|           protocol: TCP | ||||
|           name: https | ||||
| --- | ||||
| apiVersion: v1 | ||||
| kind: Service | ||||
| metadata: | ||||
|   labels: | ||||
|     app.kubernetes.io/name: "passbook-{{ outpost.type }}" | ||||
|     app.kubernetes.io/instance: "{{ outpost.name }}" | ||||
|     passbook.beryju.org/outpost: "{{ outpost.pk.hex }}" | ||||
|   name: "passbook-{{ outpost.type }}-{{ outpost.name }}" | ||||
| spec: | ||||
|   ports: | ||||
|   - name: http | ||||
|     port: 4180 | ||||
|     protocol: TCP | ||||
|     targetPort: 4180 | ||||
|   - name: https | ||||
|     port: 4443 | ||||
|     protocol: TCP | ||||
|     targetPort: 4443 | ||||
|   selector: | ||||
|     app.kubernetes.io/name: "passbook-{{ outpost.type }}" | ||||
|     app.kubernetes.io/instance: "{{ outpost.name }}" | ||||
|     passbook.beryju.org/outpost: "{{ outpost.pk.hex }}" | ||||
| --- | ||||
| apiVersion: extensions/v1beta1 | ||||
| kind: Ingress | ||||
| metadata: | ||||
|   name: "passbook-{{ outpost.type }}-{{ outpost.name }}" | ||||
| spec: | ||||
|   rules: | ||||
|   - host: "{{ provider.external_host }}" | ||||
|     http: | ||||
|       paths: | ||||
|       - backend: | ||||
|           serviceName: "passbook-{{ outpost.type }}-{{ outpost.name }}" | ||||
|           servicePort: 4180 | ||||
|         path: "/pbprox" | ||||
| </pre> | ||||
|     </div> | ||||
| </section> | ||||
| {% endblock %} | ||||
							
								
								
									
										11
									
								
								passbook/outposts/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								passbook/outposts/urls.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| """passbook outposts urls""" | ||||
| from django.urls import path | ||||
|  | ||||
| from passbook.outposts.views import KubernetesManifestView, SetupView | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path( | ||||
|         "<uuid:outpost_pk>/k8s/", KubernetesManifestView.as_view(), name="k8s-manifest" | ||||
|     ), | ||||
|     path("<uuid:outpost_pk>/", SetupView.as_view(), name="setup"), | ||||
| ] | ||||
							
								
								
									
										78
									
								
								passbook/outposts/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								passbook/outposts/views.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | ||||
| """passbook outpost views""" | ||||
| from typing import Any, Dict, List | ||||
|  | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.db.models import Model | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.views import View | ||||
| from django.views.generic import TemplateView | ||||
| from guardian.shortcuts import get_objects_for_user | ||||
| from structlog import get_logger | ||||
|  | ||||
| from passbook.core.models import User | ||||
| from passbook.outposts.controllers.compose import DockerComposeController | ||||
| from passbook.outposts.models import Outpost, OutpostType | ||||
| from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| def get_object_for_user_or_404(user: User, perm: str, **filters) -> Model: | ||||
|     """Wrapper that combines get_objects_for_user and get_object_or_404""" | ||||
|     return get_object_or_404(get_objects_for_user(user, perm), **filters) | ||||
|  | ||||
|  | ||||
| class DockerComposeView(LoginRequiredMixin, View): | ||||
|     """Generate docker-compose yaml""" | ||||
|  | ||||
|     def get(self, request: HttpRequest, outpost_pk: str) -> HttpResponse: | ||||
|         """Render docker-compose file""" | ||||
|         outpost: Outpost = get_object_for_user_or_404( | ||||
|             request.user, "passbook_outposts.view_outpost", pk=outpost_pk, | ||||
|         ) | ||||
|         manifest = "" | ||||
|         if outpost.type == OutpostType.PROXY: | ||||
|             controller = DockerComposeController(outpost_pk) | ||||
|             manifest = controller.get_static_deployment() | ||||
|  | ||||
|         return HttpResponse(manifest, content_type="text/vnd.yaml") | ||||
|  | ||||
|  | ||||
| class KubernetesManifestView(LoginRequiredMixin, View): | ||||
|     """Generate Kubernetes Deployment and SVC for proxy""" | ||||
|  | ||||
|     def get(self, request: HttpRequest, outpost_pk: str) -> HttpResponse: | ||||
|         """Render deployment template""" | ||||
|         outpost: Outpost = get_object_for_user_or_404( | ||||
|             request.user, "passbook_outposts.view_outpost", pk=outpost_pk, | ||||
|         ) | ||||
|         manifest = "" | ||||
|         if outpost.type == OutpostType.PROXY: | ||||
|             controller = ProxyKubernetesController(outpost_pk) | ||||
|             manifest = controller.get_static_deployment() | ||||
|  | ||||
|         return HttpResponse(manifest, content_type="text/vnd.yaml") | ||||
|  | ||||
|  | ||||
| class SetupView(LoginRequiredMixin, TemplateView): | ||||
|     """Setup view""" | ||||
|  | ||||
|     def get_template_names(self) -> List[str]: | ||||
|         allowed = ["dc", "custom", "k8s_manual", "k8s_integration"] | ||||
|         setup_type = self.request.GET.get("type", "dc") | ||||
|         if setup_type not in allowed: | ||||
|             setup_type = allowed[0] | ||||
|         return [f"outposts/setup_{setup_type}.html"] | ||||
|  | ||||
|     def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         outpost: Outpost = get_object_for_user_or_404( | ||||
|             self.request.user, | ||||
|             "passbook_outposts.view_outpost", | ||||
|             pk=self.kwargs["outpost_pk"], | ||||
|         ) | ||||
|         kwargs.update( | ||||
|             {"host": self.request.build_absolute_uri("/"), "outpost": outpost} | ||||
|         ) | ||||
|         return kwargs | ||||
| @ -5,9 +5,11 @@ CELERY_BEAT_SCHEDULE = { | ||||
|     "policies_reputation_ip_save": { | ||||
|         "task": "passbook.policies.reputation.tasks.save_ip_reputation", | ||||
|         "schedule": crontab(minute="*/5"), | ||||
|         "options": {"queue": "passbook_scheduled"}, | ||||
|     }, | ||||
|     "policies_reputation_user_save": { | ||||
|         "task": "passbook.policies.reputation.tasks.save_user_reputation", | ||||
|         "schedule": crontab(minute="*/5"), | ||||
|         "options": {"queue": "passbook_scheduled"}, | ||||
|     }, | ||||
| } | ||||
|  | ||||
| @ -236,7 +236,7 @@ class OAuth2Provider(Provider): | ||||
|         return OAuth2ProviderForm | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|         return f"OAuth2 Provider {self.name}" | ||||
|  | ||||
|     def html_setup_urls(self, request: HttpRequest) -> Optional[str]: | ||||
|         """return template and context modal with URLs for authorize, token, openid-config, etc""" | ||||
|  | ||||
| @ -1,4 +1,6 @@ | ||||
| """passbook OAuth2 OpenID well-known views""" | ||||
| from typing import Any, Dict | ||||
|  | ||||
| from django.http import HttpRequest, HttpResponse, JsonResponse | ||||
| from django.shortcuts import get_object_or_404, reverse | ||||
| from django.views import View | ||||
| @ -16,6 +18,41 @@ PLAN_CONTEXT_SCOPES = "scopes" | ||||
| class ProviderInfoView(View): | ||||
|     """OpenID-compliant Provider Info""" | ||||
|  | ||||
|     def get_info(self, provider: OAuth2Provider) -> Dict[str, Any]: | ||||
|         """Get dictionary for OpenID Connect information""" | ||||
|         return { | ||||
|             "issuer": provider.get_issuer(self.request), | ||||
|             "authorization_endpoint": self.request.build_absolute_uri( | ||||
|                 reverse("passbook_providers_oauth2:authorize") | ||||
|             ), | ||||
|             "token_endpoint": self.request.build_absolute_uri( | ||||
|                 reverse("passbook_providers_oauth2:token") | ||||
|             ), | ||||
|             "userinfo_endpoint": self.request.build_absolute_uri( | ||||
|                 reverse("passbook_providers_oauth2:userinfo") | ||||
|             ), | ||||
|             "end_session_endpoint": self.request.build_absolute_uri( | ||||
|                 reverse("passbook_providers_oauth2:end-session") | ||||
|             ), | ||||
|             "introspection_endpoint": self.request.build_absolute_uri( | ||||
|                 reverse("passbook_providers_oauth2:token-introspection") | ||||
|             ), | ||||
|             "response_types_supported": [provider.response_type], | ||||
|             "jwks_uri": self.request.build_absolute_uri( | ||||
|                 reverse( | ||||
|                     "passbook_providers_oauth2:jwks", | ||||
|                     kwargs={"application_slug": provider.application.slug}, | ||||
|                 ) | ||||
|             ), | ||||
|             "id_token_signing_alg_values_supported": [provider.jwt_alg], | ||||
|             # See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes | ||||
|             "subject_types_supported": ["public"], | ||||
|             "token_endpoint_auth_methods_supported": [ | ||||
|                 "client_secret_post", | ||||
|                 "client_secret_basic", | ||||
|             ], | ||||
|         } | ||||
|  | ||||
|     # pylint: disable=unused-argument | ||||
|     def get( | ||||
|         self, request: HttpRequest, application_slug: str, *args, **kwargs | ||||
| @ -26,40 +63,7 @@ class ProviderInfoView(View): | ||||
|         provider: OAuth2Provider = get_object_or_404( | ||||
|             OAuth2Provider, pk=application.provider_id | ||||
|         ) | ||||
|         response = JsonResponse( | ||||
|             { | ||||
|                 "issuer": provider.get_issuer(request), | ||||
|                 "authorization_endpoint": request.build_absolute_uri( | ||||
|                     reverse("passbook_providers_oauth2:authorize") | ||||
|                 ), | ||||
|                 "token_endpoint": request.build_absolute_uri( | ||||
|                     reverse("passbook_providers_oauth2:token") | ||||
|                 ), | ||||
|                 "userinfo_endpoint": request.build_absolute_uri( | ||||
|                     reverse("passbook_providers_oauth2:userinfo") | ||||
|                 ), | ||||
|                 "end_session_endpoint": request.build_absolute_uri( | ||||
|                     reverse("passbook_providers_oauth2:end-session") | ||||
|                 ), | ||||
|                 "introspection_endpoint": request.build_absolute_uri( | ||||
|                     reverse("passbook_providers_oauth2:token-introspection") | ||||
|                 ), | ||||
|                 "response_types_supported": [provider.response_type], | ||||
|                 "jwks_uri": request.build_absolute_uri( | ||||
|                     reverse( | ||||
|                         "passbook_providers_oauth2:jwks", | ||||
|                         kwargs={"application_slug": application.slug}, | ||||
|                     ) | ||||
|                 ), | ||||
|                 "id_token_signing_alg_values_supported": [provider.jwt_alg], | ||||
|                 # See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes | ||||
|                 "subject_types_supported": ["public"], | ||||
|                 "token_endpoint_auth_methods_supported": [ | ||||
|                     "client_secret_post", | ||||
|                     "client_secret_basic", | ||||
|                 ], | ||||
|             } | ||||
|         ) | ||||
|         response = JsonResponse(self.get_info(provider)) | ||||
|         response["Access-Control-Allow-Origin"] = "*" | ||||
|  | ||||
|         return response | ||||
|  | ||||
| @ -1,10 +1,38 @@ | ||||
| """ProxyProvider API Views""" | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from drf_yasg.utils import swagger_serializer_method | ||||
| from rest_framework.fields import CharField, ListField, SerializerMethodField | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer, Serializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from passbook.providers.oauth2.views.provider import ProviderInfoView | ||||
| from passbook.providers.proxy.models import ProxyProvider | ||||
|  | ||||
|  | ||||
| class OpenIDConnectConfigurationSerializer(Serializer): | ||||
|     """rest_framework Serializer for OIDC Configuration""" | ||||
|  | ||||
|     issuer = CharField() | ||||
|     authorization_endpoint = CharField() | ||||
|     token_endpoint = CharField() | ||||
|     userinfo_endpoint = CharField() | ||||
|     end_session_endpoint = CharField() | ||||
|     introspection_endpoint = CharField() | ||||
|     jwks_uri = CharField() | ||||
|  | ||||
|     response_types_supported = ListField(child=CharField()) | ||||
|     id_token_signing_alg_values_supported = ListField(child=CharField()) | ||||
|     subject_types_supported = ListField(child=CharField()) | ||||
|     token_endpoint_auth_methods_supported = ListField(child=CharField()) | ||||
|  | ||||
|     def create(self, request: Request) -> Response: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def update(self, request: Request) -> Response: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
| class ProxyProviderSerializer(ModelSerializer): | ||||
|     """ProxyProvider Serializer""" | ||||
|  | ||||
| @ -21,7 +49,13 @@ class ProxyProviderSerializer(ModelSerializer): | ||||
|     class Meta: | ||||
|  | ||||
|         model = ProxyProvider | ||||
|         fields = ["pk", "name", "internal_host", "external_host"] | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "name", | ||||
|             "internal_host", | ||||
|             "external_host", | ||||
|             "certificate", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class ProxyProviderViewSet(ModelViewSet): | ||||
| @ -29,3 +63,47 @@ class ProxyProviderViewSet(ModelViewSet): | ||||
|  | ||||
|     queryset = ProxyProvider.objects.all() | ||||
|     serializer_class = ProxyProviderSerializer | ||||
|  | ||||
|  | ||||
| class ProxyOutpostConfigSerializer(ModelSerializer): | ||||
|     """ProxyProvider Serializer""" | ||||
|  | ||||
|     oidc_configuration = SerializerMethodField() | ||||
|  | ||||
|     def create(self, validated_data): | ||||
|         instance: ProxyProvider = super().create(validated_data) | ||||
|         instance.set_oauth_defaults() | ||||
|         instance.save() | ||||
|         return instance | ||||
|  | ||||
|     def update(self, instance: ProxyProvider, validated_data): | ||||
|         instance.set_oauth_defaults() | ||||
|         return super().update(instance, validated_data) | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = ProxyProvider | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "name", | ||||
|             "internal_host", | ||||
|             "external_host", | ||||
|             "client_id", | ||||
|             "client_secret", | ||||
|             "oidc_configuration", | ||||
|             "cookie_secret", | ||||
|             "certificate", | ||||
|         ] | ||||
|  | ||||
|     @swagger_serializer_method(serializer_or_field=OpenIDConnectConfigurationSerializer) | ||||
|     def get_oidc_configuration(self, obj: ProxyProvider): | ||||
|         """Embed OpenID Connect provider information""" | ||||
|         # pylint: disable=protected-access | ||||
|         return ProviderInfoView(request=self.context["request"]._request).get_info(obj) | ||||
|  | ||||
|  | ||||
| class OutpostConfigViewSet(ModelViewSet): | ||||
|     """ProxyProvider Viewset""" | ||||
|  | ||||
|     queryset = ProxyProvider.objects.filter(application__isnull=False) | ||||
|     serializer_class = ProxyOutpostConfigSerializer | ||||
|  | ||||
| @ -8,4 +8,3 @@ class PassbookProviderProxyConfig(AppConfig): | ||||
|     name = "passbook.providers.proxy" | ||||
|     label = "passbook_providers_proxy" | ||||
|     verbose_name = "passbook Providers.Proxy" | ||||
|     mountpoint = "application/proxy/" | ||||
|  | ||||
							
								
								
									
										0
									
								
								passbook/providers/proxy/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/providers/proxy/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										13
									
								
								passbook/providers/proxy/controllers/kubernetes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								passbook/providers/proxy/controllers/kubernetes.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| """Proxy Provider Kubernetes Contoller""" | ||||
| from passbook.outposts.controllers.kubernetes import KubernetesController | ||||
|  | ||||
|  | ||||
| class ProxyKubernetesController(KubernetesController): | ||||
|     """Proxy Provider Kubernetes Contoller""" | ||||
|  | ||||
|     def __init__(self, outpost_pk: str): | ||||
|         super().__init__(outpost_pk) | ||||
|         self.deployment_ports = { | ||||
|             "http": 4180, | ||||
|             "https": 4443, | ||||
|         } | ||||
| @ -1,6 +1,8 @@ | ||||
| """passbook Proxy Provider Forms""" | ||||
| from django import forms | ||||
|  | ||||
| from passbook.crypto.models import CertificateKeyPair | ||||
| from passbook.flows.models import Flow, FlowDesignation | ||||
| from passbook.providers.proxy.models import ProxyProvider | ||||
|  | ||||
|  | ||||
| @ -9,14 +11,31 @@ class ProxyProviderForm(forms.ModelForm): | ||||
|  | ||||
|     instance: ProxyProvider | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.fields["authorization_flow"].queryset = Flow.objects.filter( | ||||
|             designation=FlowDesignation.AUTHORIZATION | ||||
|         ) | ||||
|         self.fields["certificate"].queryset = CertificateKeyPair.objects.filter( | ||||
|             key_data__isnull=False | ||||
|         ) | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         actual_save = super().save(*args, **kwargs) | ||||
|         self.instance.set_oauth_defaults() | ||||
|         return super().save(*args, **kwargs) | ||||
|         self.instance.save() | ||||
|         return actual_save | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = ProxyProvider | ||||
|         fields = ["name", "authorization_flow", "internal_host", "external_host"] | ||||
|         fields = [ | ||||
|             "name", | ||||
|             "authorization_flow", | ||||
|             "internal_host", | ||||
|             "external_host", | ||||
|             "certificate", | ||||
|         ] | ||||
|         widgets = { | ||||
|             "name": forms.TextInput(), | ||||
|             "internal_host": forms.TextInput(), | ||||
|  | ||||
| @ -0,0 +1,22 @@ | ||||
| # Generated by Django 3.1 on 2020-08-19 14:50 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
| import passbook.providers.proxy.models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("passbook_providers_proxy", "0001_initial"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="proxyprovider", | ||||
|             name="cookie_secret", | ||||
|             field=models.TextField( | ||||
|                 default=passbook.providers.proxy.models.get_cookie_secret | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -0,0 +1,24 @@ | ||||
| # Generated by Django 3.1 on 2020-08-23 22:46 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("passbook_crypto", "0002_create_self_signed_kp"), | ||||
|         ("passbook_providers_proxy", "0002_proxyprovider_cookie_secret"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="proxyprovider", | ||||
|             name="certificate", | ||||
|             field=models.ForeignKey( | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_NULL, | ||||
|                 to="passbook_crypto.certificatekeypair", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -1,13 +1,16 @@ | ||||
| """passbook proxy models""" | ||||
| from typing import Optional, Type | ||||
| import string | ||||
| from random import SystemRandom | ||||
| from typing import Iterable, Type | ||||
| from urllib.parse import urljoin | ||||
|  | ||||
| from django.core.validators import URLValidator | ||||
| from django.db import models | ||||
| from django.forms import ModelForm | ||||
| from django.http import HttpRequest | ||||
| from django.utils.translation import gettext as _ | ||||
|  | ||||
| from passbook.lib.utils.template import render_to_string | ||||
| from passbook.crypto.models import CertificateKeyPair | ||||
| from passbook.outposts.models import OutpostModel | ||||
| from passbook.providers.oauth2.constants import ( | ||||
|     SCOPE_OPENID, | ||||
|     SCOPE_OPENID_EMAIL, | ||||
| @ -22,7 +25,18 @@ from passbook.providers.oauth2.models import ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| class ProxyProvider(OAuth2Provider): | ||||
| def get_cookie_secret(): | ||||
|     """Generate random 32-character string for cookie-secret""" | ||||
|     return "".join( | ||||
|         SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32) | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def _get_callback_url(uri: str) -> str: | ||||
|     return urljoin(uri, "/pbprox/callback") | ||||
|  | ||||
|  | ||||
| class ProxyProvider(OutpostModel, OAuth2Provider): | ||||
|     """Protect applications that don't support any of the other | ||||
|     Protocols by using a Reverse-Proxy.""" | ||||
|  | ||||
| @ -33,39 +47,42 @@ class ProxyProvider(OAuth2Provider): | ||||
|         validators=[URLValidator(schemes=("http", "https"))] | ||||
|     ) | ||||
|  | ||||
|     cookie_secret = models.TextField(default=get_cookie_secret) | ||||
|  | ||||
|     certificate = models.ForeignKey( | ||||
|         CertificateKeyPair, on_delete=models.SET_NULL, null=True | ||||
|     ) | ||||
|  | ||||
|     def form(self) -> Type[ModelForm]: | ||||
|         from passbook.providers.proxy.forms import ProxyProviderForm | ||||
|  | ||||
|         return ProxyProviderForm | ||||
|  | ||||
|     def html_setup_urls(self, request: HttpRequest) -> Optional[str]: | ||||
|         """return template and context modal with URLs for authorize, token, openid-config, etc""" | ||||
|         from passbook.providers.proxy.views import DockerComposeView | ||||
|  | ||||
|         docker_compose_yaml = DockerComposeView(request=request).get_compose(self) | ||||
|         return render_to_string( | ||||
|             "providers/proxy/setup_modal.html", | ||||
|             {"provider": self, "docker_compose": docker_compose_yaml}, | ||||
|         ) | ||||
|  | ||||
|     def set_oauth_defaults(self): | ||||
|         """Ensure all OAuth2-related settings are correct""" | ||||
|         self.client_type = ClientTypes.CONFIDENTIAL | ||||
|         self.response_type = ResponseTypes.CODE | ||||
|         self.jwt_alg = JWTAlgorithms.HS256 | ||||
|         self.jwt_alg = JWTAlgorithms.RS256 | ||||
|         self.rsa_key = CertificateKeyPair.objects.first() | ||||
|         scopes = ScopeMapping.objects.filter( | ||||
|             scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_PROFILE, SCOPE_OPENID_EMAIL] | ||||
|         ) | ||||
|         self.property_mappings.set(scopes) | ||||
|         self.redirect_uris = "\n".join( | ||||
|             [ | ||||
|                 f"{self.external_host}/oauth2/callback", | ||||
|                 f"{self.internal_host}/oauth2/callback", | ||||
|                 _get_callback_url(self.external_host), | ||||
|                 _get_callback_url(self.internal_host), | ||||
|             ] | ||||
|         ) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|         return f"Proxy Provider {self.name}" | ||||
|  | ||||
|     def get_required_objects(self) -> Iterable[models.Model]: | ||||
|         required_models = [self] | ||||
|         if self.certificate is not None: | ||||
|             required_models.append(self.certificate) | ||||
|         return required_models | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|  | ||||
| @ -1,72 +0,0 @@ | ||||
| apiVersion: apps/v1 | ||||
| kind: Deployment | ||||
| metadata: | ||||
|   labels: | ||||
|     app.kubernetes.io/name: "passbook-gatekeeper-{{ provider.name }}" | ||||
|     passbook.beryju.org/gatekeeper/provider: "{{ provider.pk }}" | ||||
|   name: passbook-gatekeeper | ||||
| spec: | ||||
|   replicas: 1 | ||||
|   selector: | ||||
|     matchLabels: | ||||
|       app.kubernetes.io/name: passbook-gatekeeper | ||||
|       passbook.beryju.org/gatekeeper/provider: "{{ provider.pk }}" | ||||
|   template: | ||||
|     metadata: | ||||
|       labels: | ||||
|         app.kubernetes.io/name: passbook-gatekeeper | ||||
|         passbook.beryju.org/gatekeeper/provider: "{{ provider.pk }}" | ||||
|     spec: | ||||
|       containers: | ||||
|       - args: | ||||
|         - --upstream=file:///dev/null | ||||
|         env: | ||||
|         - name: OAUTH2_PROXY_CLIENT_ID | ||||
|           value: "{{ provider.client.client_id }}" | ||||
|         - name: OAUTH2_PROXY_CLIENT_SECRET | ||||
|           value: "{{ provider.client.client_secret }}" | ||||
|         - name: OAUTH2_PROXY_COOKIE_SECRET | ||||
|           value: "{{ cookie_secret }}" | ||||
|         - name: OAUTH2_PROXY_OIDC_ISSUER_URL | ||||
|           value: "{{ issuer }}" | ||||
|         - name: OAUTH2_PROXY_SET_XAUTHREQUEST | ||||
|           value: "true" | ||||
|         - name: OAUTH2_PROXY_SET_AUTHORIZATION_HEADER | ||||
|           value: "true" | ||||
|         image: beryju/passbook-gatekeeper:{{ version }} | ||||
|         imagePullPolicy: Always | ||||
|         name: passbook-gatekeeper | ||||
|         ports: | ||||
|         - containerPort: 4180 | ||||
|           protocol: TCP | ||||
| --- | ||||
| apiVersion: v1 | ||||
| kind: Service | ||||
| metadata: | ||||
|   labels: | ||||
|     app.kubernetes.io/name: "passbook-gatekeeper-{{ provider.name }}" | ||||
|     passbook.beryju.org/gatekeeper/provider: "{{ provider.pk }}" | ||||
|   name: passbook-gatekeeper | ||||
| spec: | ||||
|   ports: | ||||
|   - name: http | ||||
|     port: 4180 | ||||
|     protocol: TCP | ||||
|     targetPort: 4180 | ||||
|   selector: | ||||
|     app.kubernetes.io/name: passbook-gatekeeper | ||||
|     passbook.beryju.org/gatekeeper/provider: "{{ provider.pk }}" | ||||
| --- | ||||
| apiVersion: extensions/v1beta1 | ||||
| kind: Ingress | ||||
| metadata: | ||||
|   name: passbook-gatekeeper-{{ provider.name }} | ||||
| spec: | ||||
|   rules: | ||||
|   - host: {{ provider.external_host }} | ||||
|     http: | ||||
|       paths: | ||||
|       - backend: | ||||
|           serviceName: "passbook-gatekeeper-{{ provider.name }}" | ||||
|           servicePort: 4180 | ||||
|         path: /oauth2 | ||||
| @ -1,10 +0,0 @@ | ||||
| """passbook proxy urls""" | ||||
| from django.urls import path | ||||
|  | ||||
| from passbook.providers.proxy.views import K8sManifestView | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path( | ||||
|         "<int:provider>/k8s-manifest/", K8sManifestView.as_view(), name="k8s-manifest" | ||||
|     ), | ||||
| ] | ||||
| @ -1,96 +0,0 @@ | ||||
| """passbook proxy views""" | ||||
| import string | ||||
| from random import SystemRandom | ||||
| from urllib.parse import urlparse | ||||
|  | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.db.models import Model | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.shortcuts import get_object_or_404, render | ||||
| from django.views import View | ||||
| from guardian.shortcuts import get_objects_for_user | ||||
| from structlog import get_logger | ||||
| from yaml import safe_dump | ||||
|  | ||||
| from passbook import __version__ | ||||
| from passbook.core.models import User | ||||
| from passbook.providers.proxy.models import ProxyProvider | ||||
|  | ||||
| ORIGINAL_URL = "HTTP_X_ORIGINAL_URL" | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| def get_object_for_user_or_404(user: User, perm: str, **filters) -> Model: | ||||
|     """Wrapper that combines get_objects_for_user and get_object_or_404""" | ||||
|     return get_object_or_404(get_objects_for_user(user, perm), **filters) | ||||
|  | ||||
|  | ||||
| def get_cookie_secret(): | ||||
|     """Generate random 32-character string for cookie-secret""" | ||||
|     return "".join( | ||||
|         SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32) | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class DockerComposeView(LoginRequiredMixin, View): | ||||
|     """Generate docker-compose yaml""" | ||||
|  | ||||
|     def get_compose(self, provider: ProxyProvider) -> str: | ||||
|         """Generate docker-compose yaml, version 3.5""" | ||||
|         issuer = provider.get_issuer(self.request) | ||||
|         env = { | ||||
|             "OAUTH2_PROXY_CLIENT_ID": provider.client_id, | ||||
|             "OAUTH2_PROXY_CLIENT_SECRET": provider.client_secret, | ||||
|             "OAUTH2_PROXY_REDIRECT_URL": f"{provider.external_host}/oauth2/callback", | ||||
|             "OAUTH2_PROXY_OIDC_ISSUER_URL": issuer, | ||||
|             "OAUTH2_PROXY_COOKIE_SECRET": get_cookie_secret(), | ||||
|             "OAUTH2_PROXY_UPSTREAMS": provider.internal_host, | ||||
|         } | ||||
|         if urlparse(provider.external_host).scheme != "https": | ||||
|             env["OAUTH2_PROXY_COOKIE_SECURE"] = "false" | ||||
|         compose = { | ||||
|             "version": "3.5", | ||||
|             "services": { | ||||
|                 "passbook_gatekeeper": { | ||||
|                     "image": f"beryju/passbook-proxy:{__version__}", | ||||
|                     "ports": ["4180:4180"], | ||||
|                     "environment": env, | ||||
|                 } | ||||
|             }, | ||||
|         } | ||||
|         return safe_dump(compose, default_flow_style=False) | ||||
|  | ||||
|     def get(self, request: HttpRequest, provider_pk: int) -> HttpResponse: | ||||
|         """Render docker-compose file""" | ||||
|         provider: ProxyProvider = get_object_for_user_or_404( | ||||
|             request.user, | ||||
|             "passbook_providers_proxy.view_applicationgatewayprovider", | ||||
|             pk=provider_pk, | ||||
|         ) | ||||
|         response = HttpResponse() | ||||
|         response.content_type = "application/x-yaml" | ||||
|         response.content = self.get_compose(provider.pk) | ||||
|         return response | ||||
|  | ||||
|  | ||||
| class K8sManifestView(LoginRequiredMixin, View): | ||||
|     """Generate K8s Deployment and SVC for gatekeeper""" | ||||
|  | ||||
|     def get(self, request: HttpRequest, provider_pk: int) -> HttpResponse: | ||||
|         """Render deployment template""" | ||||
|         provider: ProxyProvider = get_object_for_user_or_404( | ||||
|             request.user, | ||||
|             "passbook_providers_app_gw.view_applicationgatewayprovider", | ||||
|             pk=provider_pk, | ||||
|         ) | ||||
|         return render( | ||||
|             request, | ||||
|             "providers/proxy/k8s-manifest.yaml", | ||||
|             { | ||||
|                 "provider": provider, | ||||
|                 "cookie_secret": get_cookie_secret(), | ||||
|                 "version": __version__, | ||||
|                 "issuer": provider.get_issuer(request), | ||||
|             }, | ||||
|             content_type="text/yaml", | ||||
|         ) | ||||
| @ -108,7 +108,7 @@ class SAMLProvider(Provider): | ||||
|         return SAMLProviderForm | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|         return f"SAML Provider {self.name}" | ||||
|  | ||||
|     def link_download_metadata(self): | ||||
|         """Get link to download XML metadata for admin interface""" | ||||
|  | ||||
							
								
								
									
										120
									
								
								passbook/root/asgi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								passbook/root/asgi.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | ||||
| """ | ||||
| ASGI config for passbook project. | ||||
|  | ||||
| It exposes the ASGI callable as a module-level variable named ``application``. | ||||
|  | ||||
| For more information on this file, see | ||||
| https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ | ||||
| """ | ||||
| import os | ||||
| import typing | ||||
| from time import time | ||||
| from typing import Any, ByteString, Dict | ||||
|  | ||||
| import django | ||||
| from asgiref.compatibility import guarantee_single_callable | ||||
| from channels.routing import get_default_application | ||||
| from defusedxml import defuse_stdlib | ||||
| from sentry_sdk.integrations.asgi import SentryAsgiMiddleware | ||||
| from structlog import get_logger | ||||
|  | ||||
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings") | ||||
|  | ||||
| defuse_stdlib() | ||||
| django.setup() | ||||
|  | ||||
| # See https://github.com/encode/starlette/blob/master/starlette/types.py | ||||
| Scope = typing.MutableMapping[str, typing.Any] | ||||
| Message = typing.MutableMapping[str, typing.Any] | ||||
|  | ||||
| Receive = typing.Callable[[], typing.Awaitable[Message]] | ||||
| Send = typing.Callable[[Message], typing.Awaitable[None]] | ||||
|  | ||||
| ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]] | ||||
|  | ||||
| LOGGER = get_logger("passbook.asgi") | ||||
|  | ||||
|  | ||||
| class ASGILoggerMiddleware: | ||||
|     """Main ASGI Logger middleware, starts an ASGILogger for each request""" | ||||
|  | ||||
|     def __init__(self, app: ASGIApp) -> None: | ||||
|         self.app = app | ||||
|  | ||||
|     async def __call__(self, scope: Scope, receive: Receive, send: Send): | ||||
|         responder = ASGILogger(self.app) | ||||
|         await responder(scope, receive, send) | ||||
|         return | ||||
|  | ||||
|  | ||||
| class ASGILogger: | ||||
|     """ASGI Logger, instantiated for each request""" | ||||
|  | ||||
|     app: ASGIApp | ||||
|     send: Send | ||||
|  | ||||
|     scope: Scope | ||||
|     headers: Dict[ByteString, Any] | ||||
|  | ||||
|     status_code: int | ||||
|     start: float | ||||
|     content_length: int | ||||
|  | ||||
|     def __init__(self, app: ASGIApp): | ||||
|         self.app = app | ||||
|  | ||||
|     async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: | ||||
|         self.send = send | ||||
|         self.scope = scope | ||||
|         self.content_length = 0 | ||||
|         self.headers = dict(scope.get("headers", [])) | ||||
|  | ||||
|         if self.headers.get(b"host", b"") == b"kubernetes-healthcheck-host": | ||||
|             # Don't log kubernetes health/readiness requests | ||||
|             await send({"type": "http.response.start", "status": 204, "headers": []}) | ||||
|             await send({"type": "http.response.body", "body": ""}) | ||||
|             return | ||||
|  | ||||
|         self.start = time() | ||||
|         await self.app(scope, receive, self.send_hooked) | ||||
|  | ||||
|     async def send_hooked(self, message: Message) -> None: | ||||
|         """Hooked send method, which records status code and content-length, and for the final | ||||
|         requests logs it""" | ||||
|         headers = dict(message.get("headers", [])) | ||||
|  | ||||
|         if "status" in message: | ||||
|             self.status_code = message["status"] | ||||
|  | ||||
|         if b"Content-Length" in headers: | ||||
|             self.content_length += int(headers.get(b"Content-Length", b"0")) | ||||
|  | ||||
|         if message["type"] == "http.response.body" and not message["more_body"]: | ||||
|             runtime = int((time() - self.start) * 10 ** 6) | ||||
|             self.log(runtime) | ||||
|         return await self.send(message) | ||||
|  | ||||
|     def _get_ip(self) -> str: | ||||
|         client_ip, _ = self.scope.get("client", ("", 0)) | ||||
|         return client_ip | ||||
|  | ||||
|     def log(self, runtime: float): | ||||
|         """Outpot access logs in a structured format""" | ||||
|         host = self._get_ip() | ||||
|         query_string = "" | ||||
|         if self.scope.get("query_string", b"") != b"": | ||||
|             query_string = f"?{self.scope.get('query_string').decode()}" | ||||
|         LOGGER.info( | ||||
|             f"{self.scope.get('path', '')}{query_string}", | ||||
|             host=host, | ||||
|             method=self.scope.get("method", ""), | ||||
|             scheme=self.scope.get("scheme", ""), | ||||
|             status=self.status_code, | ||||
|             size=self.content_length / 1000 if self.content_length > 0 else "-", | ||||
|             runtime=runtime, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| application = SentryAsgiMiddleware( | ||||
|     ASGILogger(guarantee_single_callable(get_default_application())) | ||||
| ) | ||||
							
								
								
									
										12
									
								
								passbook/root/routing.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								passbook/root/routing.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| """root Websocket URLS""" | ||||
| from channels.routing import ProtocolTypeRouter, URLRouter | ||||
| from django.urls import path | ||||
|  | ||||
| from passbook.outposts.channels import OutpostConsumer | ||||
|  | ||||
| application = ProtocolTypeRouter( | ||||
|     { | ||||
|         # (http->django views is added by default) | ||||
|         "websocket": URLRouter([path("ws/outpost/<uuid:pk>/", OutpostConsumer)]), | ||||
|     } | ||||
| ) | ||||
| @ -73,11 +73,13 @@ INSTALLED_APPS = [ | ||||
|     "drf_yasg", | ||||
|     "guardian", | ||||
|     "django_prometheus", | ||||
|     "channels", | ||||
|     "passbook.admin.apps.PassbookAdminConfig", | ||||
|     "passbook.api.apps.PassbookAPIConfig", | ||||
|     "passbook.audit.apps.PassbookAuditConfig", | ||||
|     "passbook.crypto.apps.PassbookCryptoConfig", | ||||
|     "passbook.flows.apps.PassbookFlowsConfig", | ||||
|     "passbook.outposts.apps.PassbookOutpostConfig", | ||||
|     "passbook.lib.apps.PassbookLibConfig", | ||||
|     "passbook.policies.apps.PassbookPoliciesConfig", | ||||
|     "passbook.policies.dummy.apps.PassbookPolicyDummyConfig", | ||||
| @ -125,13 +127,13 @@ REST_FRAMEWORK = { | ||||
|     "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", | ||||
|     "PAGE_SIZE": 100, | ||||
|     "DEFAULT_FILTER_BACKENDS": [ | ||||
|         "rest_framework_guardian.filters.ObjectPermissionsFilter", | ||||
|         "django_filters.rest_framework.DjangoFilterBackend", | ||||
|         "rest_framework.filters.OrderingFilter", | ||||
|         "rest_framework.filters.SearchFilter", | ||||
|     ], | ||||
|     "DEFAULT_PERMISSION_CLASSES": ( | ||||
|         "rest_framework.permissions.DjangoObjectPermissions", | ||||
|         "passbook.api.permissions.CustomObjectPermissions", | ||||
|     ), | ||||
|     "DEFAULT_AUTHENTICATION_CLASSES": ( | ||||
|         "passbook.api.auth.PassbookTokenAuthentication", | ||||
| @ -185,7 +187,15 @@ TEMPLATES = [ | ||||
|     }, | ||||
| ] | ||||
|  | ||||
| WSGI_APPLICATION = "passbook.root.wsgi.application" | ||||
| ASGI_APPLICATION = "passbook.root.routing.application" | ||||
|  | ||||
| CHANNEL_LAYERS = { | ||||
|     "default": { | ||||
|         "BACKEND": "channels_redis.core.RedisChannelLayer", | ||||
|         "CONFIG": {"hosts": [(CONFIG.y("redis.host"), 6379)]}, | ||||
|     }, | ||||
| } | ||||
|  | ||||
|  | ||||
| # Database | ||||
| # https://docs.djangoproject.com/en/2.1/ref/settings/#databases | ||||
| @ -234,6 +244,7 @@ CELERY_BEAT_SCHEDULE = { | ||||
|     "clean_expired_models": { | ||||
|         "task": "passbook.core.tasks.clean_expired_models", | ||||
|         "schedule": crontab(minute="*/5"),  # Run every 5 minutes | ||||
|         "options": {"queue": "passbook_scheduled"}, | ||||
|     } | ||||
| } | ||||
| CELERY_CREATE_MISSING_QUEUES = True | ||||
| @ -364,6 +375,7 @@ _LOGGING_HANDLER_MAP = { | ||||
|     "grpc": LOG_LEVEL, | ||||
|     "docker": "WARNING", | ||||
|     "urllib3": "WARNING", | ||||
|     "websockets": "WARNING", | ||||
| } | ||||
| for handler_name, level in _LOGGING_HANDLER_MAP.items(): | ||||
|     # pyright: reportGeneralTypeIssues=false | ||||
|  | ||||
| @ -1,78 +0,0 @@ | ||||
| """ | ||||
| WSGI config for passbook project. | ||||
|  | ||||
| It exposes the WSGI callable as a module-level variable named ``application``. | ||||
|  | ||||
| For more information on this file, see | ||||
| https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ | ||||
| """ | ||||
| import os | ||||
| from time import time | ||||
|  | ||||
| from defusedxml import defuse_stdlib | ||||
| from django.core.wsgi import get_wsgi_application | ||||
| from structlog import get_logger | ||||
|  | ||||
| from passbook.lib.utils.http import _get_client_ip_from_meta | ||||
|  | ||||
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings") | ||||
| defuse_stdlib() | ||||
|  | ||||
|  | ||||
| class WSGILogger: | ||||
|     """ This is the generalized WSGI middleware for any style request logging. """ | ||||
|  | ||||
|     def __init__(self, _application): | ||||
|         self.application = _application | ||||
|         self.logger = get_logger("passbook.wsgi") | ||||
|  | ||||
|     def __healthcheck(self, start_response): | ||||
|         start_response("204 OK", []) | ||||
|         return [b""] | ||||
|  | ||||
|     def __call__(self, environ, start_response): | ||||
|         start = time() | ||||
|         status_codes = [] | ||||
|         content_lengths = [] | ||||
|  | ||||
|         if environ.get("HTTP_HOST", "").startswith("kubernetes-healthcheck-host"): | ||||
|             # Don't log kubernetes health/readiness requests | ||||
|             return self.__healthcheck(start_response) | ||||
|  | ||||
|         def custom_start_response(status, response_headers, exc_info=None): | ||||
|             status_codes.append(int(status.partition(" ")[0])) | ||||
|             for name, value in response_headers: | ||||
|                 if name.lower() == "content-length": | ||||
|                     content_lengths.append(int(value)) | ||||
|                     break | ||||
|             return start_response(status, response_headers, exc_info) | ||||
|  | ||||
|         retval = self.application(environ, custom_start_response) | ||||
|         runtime = int((time() - start) * 10 ** 6) | ||||
|         content_length = content_lengths[0] if content_lengths else 0 | ||||
|         self.log(status_codes[0], environ, content_length, runtime=runtime) | ||||
|         return retval | ||||
|  | ||||
|     def log(self, status_code, environ, content_length, **kwargs): | ||||
|         """ | ||||
|         Apache log format 'NCSA extended/combined log': | ||||
|         "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" | ||||
|         see http://httpd.apache.org/docs/current/mod/mod_log_config.html#formats | ||||
|         """ | ||||
|         host = _get_client_ip_from_meta(environ) | ||||
|         query_string = "" | ||||
|         if environ.get("QUERY_STRING") != "": | ||||
|             query_string = f"?{environ.get('QUERY_STRING')}" | ||||
|         self.logger.info( | ||||
|             "request", | ||||
|             path=f"{environ.get('PATH_INFO', '')}{query_string}", | ||||
|             host=host, | ||||
|             method=environ.get("REQUEST_METHOD", ""), | ||||
|             protocol=environ.get("SERVER_PROTOCOL", ""), | ||||
|             status=status_code, | ||||
|             size=content_length / 1000 if content_length > 0 else "-", | ||||
|             runtime=kwargs.get("runtime"), | ||||
|         ) | ||||
|  | ||||
|  | ||||
| application = WSGILogger(get_wsgi_application()) | ||||
| @ -9,5 +9,6 @@ CELERY_BEAT_SCHEDULE = { | ||||
|     "sources_ldap_sync": { | ||||
|         "task": "passbook.sources.ldap.tasks.sync", | ||||
|         "schedule": crontab(minute=0),  # Run every hour | ||||
|         "options": {"queue": "passbook_scheduled"}, | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -5,5 +5,6 @@ CELERY_BEAT_SCHEDULE = { | ||||
|     "saml_source_cleanup": { | ||||
|         "task": "passbook.sources.saml.tasks.clean_temporary_users", | ||||
|         "schedule": crontab(minute="*/5"), | ||||
|         "options": {"queue": "passbook_scheduled"}, | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| # Generated by Django 3.1 on 2020-08-28 13:14 | ||||
| # Generated by Django 3.1 on 2020-08-23 22:46 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
							
								
								
									
										2
									
								
								proxy/.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								proxy/.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| Dockerfile.* | ||||
| .git | ||||
							
								
								
									
										2
									
								
								proxy/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								proxy/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| pkg/client/ | ||||
| pkg/models/ | ||||
							
								
								
									
										12
									
								
								proxy/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								proxy/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| FROM golang:1.15 AS builder | ||||
|  | ||||
| WORKDIR /work | ||||
|  | ||||
| COPY . . | ||||
|  | ||||
| RUN go build -o /work/proxy . | ||||
|  | ||||
| # Copy binary to alpine | ||||
| FROM gcr.io/distroless/base-debian10 | ||||
| COPY --from=builder /work/proxy / | ||||
| ENTRYPOINT ["/proxy"] | ||||
							
								
								
									
										6
									
								
								proxy/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								proxy/Makefile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| generate: | ||||
| 	go get -u github.com/go-swagger/go-swagger/cmd/swagger | ||||
| 	swagger generate client -f ../swagger.yaml -A passbook -t pkg/ | ||||
|  | ||||
| run: | ||||
| 	go run -v . | ||||
							
								
								
									
										24
									
								
								proxy/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								proxy/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| # passbook Proxy | ||||
|  | ||||
| [](https://dev.azure.com/beryjuorg/passbook/_build?definitionId=3) | ||||
|  | ||||
|  | ||||
| Reverse Proxy based on [oauth2_proxy](https://github.com/oauth2-proxy/oauth2-proxy), completely managed and monitored by passbook. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| passbook Proxy is built to be configured by passbook itself, hence the only options you can directly give it are connection params. | ||||
|  | ||||
| The following environment variable are implemented: | ||||
|  | ||||
| `PASSBOOK_HOST`: Full URL to the passbook instance with protocol, i.e. "https://passbook.company.tld" | ||||
|  | ||||
| `PASSBOOK_TOKEN`: Token used to authenticate against passbook. This is generated after an Outpost instance is created. | ||||
|  | ||||
| `PASSBOOK_INSECURE`: This environment variable can optionally be set to ignore the SSL Certificate of the passbook instance. Applies to both HTTP and WS connections. | ||||
|  | ||||
| ## Development | ||||
|  | ||||
| passbook Proxy uses an auto-generated API Client to communicate with passbook. This client is not kept in git. To generate the client locally, run `make generate`. | ||||
|  | ||||
| Afterwards you can build the proxy like any other Go project, using `go build`. | ||||
							
								
								
									
										90
									
								
								proxy/azure-pipelines.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								proxy/azure-pipelines.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | ||||
| trigger: | ||||
|   - master | ||||
|  | ||||
| stages: | ||||
|   - stage: generate | ||||
|     jobs: | ||||
|       - job: swagger_generate | ||||
|         pool: | ||||
|           vmImage: 'ubuntu-latest' | ||||
|         steps: | ||||
|           - task: GoTool@0 | ||||
|             inputs: | ||||
|               version: '1.15' | ||||
|           - task: Go@0 | ||||
|             inputs: | ||||
|               command: 'get' | ||||
|               arguments: '-u github.com/go-swagger/go-swagger/cmd/swagger' | ||||
|           - task: CmdLine@2 | ||||
|             inputs: | ||||
|               script: | | ||||
|                 $(go list -f {{.Target}} github.com/go-swagger/go-swagger/cmd/swagger) generate client -f ../swagger.yaml -A passbook -t pkg/ | ||||
|               workingDirectory: 'proxy/' | ||||
|           - task: PublishPipelineArtifact@1 | ||||
|             inputs: | ||||
|               targetPath: 'proxy/pkg/' | ||||
|               artifact: 'swagger_client' | ||||
|               publishLocation: 'pipeline' | ||||
|   - stage: lint | ||||
|     jobs: | ||||
|       - job: golint | ||||
|         pool: | ||||
|           vmImage: 'ubuntu-latest' | ||||
|         steps: | ||||
|           - task: GoTool@0 | ||||
|             inputs: | ||||
|               version: '1.15' | ||||
|           - task: Go@0 | ||||
|             inputs: | ||||
|               command: 'get' | ||||
|               arguments: '-u golang.org/x/lint/golint' | ||||
|           - task: DownloadPipelineArtifact@2 | ||||
|             inputs: | ||||
|               buildType: 'current' | ||||
|               artifactName: 'swagger_client' | ||||
|               path: "proxy/pkg/" | ||||
|           - task: CmdLine@2 | ||||
|             inputs: | ||||
|               script: | | ||||
|                 $(go list -f {{.Target}} golang.org/x/lint/golint) ./... | ||||
|               workingDirectory: 'proxy/' | ||||
|   - stage: build_go | ||||
|     jobs: | ||||
|       - job: build_go | ||||
|         pool: | ||||
|           vmImage: 'ubuntu-latest' | ||||
|         steps: | ||||
|           - task: GoTool@0 | ||||
|             inputs: | ||||
|               version: '1.15' | ||||
|           - task: DownloadPipelineArtifact@2 | ||||
|             inputs: | ||||
|               buildType: 'current' | ||||
|               artifactName: 'swagger_client' | ||||
|               path: "proxy/pkg/" | ||||
|           - task: Go@0 | ||||
|             inputs: | ||||
|               command: 'build' | ||||
|               workingDirectory: 'proxy/' | ||||
|   - stage: build_docker | ||||
|     jobs: | ||||
|       - job: build_proxy | ||||
|         pool: | ||||
|           vmImage: 'ubuntu-latest' | ||||
|         steps: | ||||
|           - task: GoTool@0 | ||||
|             inputs: | ||||
|               version: '1.15' | ||||
|           - task: DownloadPipelineArtifact@2 | ||||
|             inputs: | ||||
|               buildType: 'current' | ||||
|               artifactName: 'swagger_client' | ||||
|               path: "proxy/pkg/" | ||||
|           - task: Docker@2 | ||||
|             inputs: | ||||
|               containerRegistry: 'dockerhub' | ||||
|               repository: 'beryju/passbook-proxy' | ||||
|               command: 'buildAndPush' | ||||
|               Dockerfile: 'proxy/Dockerfile' | ||||
|               buildContext: 'proxy/' | ||||
|               tags: 'gh-$(Build.SourceBranchName)' | ||||
							
								
								
									
										45
									
								
								proxy/cmd/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								proxy/cmd/server.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"math/rand" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/BeryJu/passbook/proxy/pkg/server" | ||||
| ) | ||||
|  | ||||
| // RunServer main entrypoint, runs the full server | ||||
| func RunServer() { | ||||
| 	pbURL, found := os.LookupEnv("PASSBOOK_HOST") | ||||
| 	if !found { | ||||
| 		panic("env PASSBOOK_HOST not set!") | ||||
| 	} | ||||
| 	pbToken, found := os.LookupEnv("PASSBOOK_TOKEN") | ||||
| 	if !found { | ||||
| 		panic("env PASSBOOK_TOKEN not set!") | ||||
| 	} | ||||
|  | ||||
| 	pbURLActual, err := url.Parse(pbURL) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	rand.Seed(time.Now().UnixNano()) | ||||
|  | ||||
| 	ac := server.NewAPIController(*pbURLActual, pbToken) | ||||
|  | ||||
| 	interrupt := make(chan os.Signal, 1) | ||||
| 	signal.Notify(interrupt, os.Interrupt) | ||||
|  | ||||
| 	ac.Start() | ||||
|  | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-interrupt: | ||||
| 			ac.Shutdown() | ||||
| 			os.Exit(0) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										44
									
								
								proxy/go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								proxy/go.mod
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| module github.com/BeryJu/passbook/proxy | ||||
|  | ||||
| go 1.14 | ||||
|  | ||||
| require ( | ||||
| 	cloud.google.com/go v0.64.0 // indirect | ||||
| 	github.com/asaskevich/govalidator v0.0.0-20200819183940-29e1ff8eb0bb // indirect | ||||
| 	github.com/coreos/go-oidc v2.2.1+incompatible | ||||
| 	github.com/getsentry/sentry-go v0.7.0 | ||||
| 	github.com/go-openapi/errors v0.19.6 | ||||
| 	github.com/go-openapi/runtime v0.19.21 | ||||
| 	github.com/go-openapi/spec v0.19.9 // indirect | ||||
| 	github.com/go-openapi/strfmt v0.19.5 | ||||
| 	github.com/go-openapi/swag v0.19.9 | ||||
| 	github.com/go-openapi/validate v0.19.10 | ||||
| 	github.com/go-redis/redis/v7 v7.4.0 // indirect | ||||
| 	github.com/go-swagger/go-swagger v0.25.0 // indirect | ||||
| 	github.com/gorilla/handlers v1.5.0 // indirect | ||||
| 	github.com/gorilla/websocket v1.4.2 | ||||
| 	github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a | ||||
| 	github.com/justinas/alice v1.2.0 | ||||
| 	github.com/kr/pretty v0.2.1 // indirect | ||||
| 	github.com/magiconair/properties v1.8.2 // indirect | ||||
| 	github.com/mailru/easyjson v0.7.6 // indirect | ||||
| 	github.com/mitchellh/mapstructure v1.3.3 // indirect | ||||
| 	github.com/oauth2-proxy/oauth2-proxy v1.1.2-0.20200817154438-5fa5b3186f39 | ||||
| 	github.com/pelletier/go-toml v1.8.0 // indirect | ||||
| 	github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f // indirect | ||||
| 	github.com/recws-org/recws v1.2.1 | ||||
| 	github.com/sirupsen/logrus v1.4.2 | ||||
| 	github.com/spf13/afero v1.3.4 // indirect | ||||
| 	github.com/spf13/cast v1.3.1 // indirect | ||||
| 	github.com/spf13/jwalterweatherman v1.1.0 // indirect | ||||
| 	github.com/spf13/pflag v1.0.5 // indirect | ||||
| 	github.com/spf13/viper v1.7.1 // indirect | ||||
| 	github.com/stretchr/testify v1.6.1 | ||||
| 	go.mongodb.org/mongo-driver v1.4.0 // indirect | ||||
| 	golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de // indirect | ||||
| 	golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect | ||||
| 	golang.org/x/sys v0.0.0-20200828194041-157a740278f4 // indirect | ||||
| 	golang.org/x/tools v0.0.0-20200828161849-5deb26317202 // indirect | ||||
| 	gopkg.in/ini.v1 v1.60.2 // indirect | ||||
| 	gopkg.in/square/go-jose.v2 v2.5.1 // indirect | ||||
| ) | ||||
							
								
								
									
										1042
									
								
								proxy/go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1042
									
								
								proxy/go.sum
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										11
									
								
								proxy/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								proxy/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"github.com/BeryJu/passbook/proxy/cmd" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	log.SetLevel(log.DebugLevel) | ||||
| 	cmd.RunServer() | ||||
| } | ||||
							
								
								
									
										1039
									
								
								proxy/pkg/proxy/oauthproxy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1039
									
								
								proxy/pkg/proxy/oauthproxy.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										187
									
								
								proxy/pkg/proxy/templates.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								proxy/pkg/proxy/templates.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,187 @@ | ||||
| package proxy | ||||
|  | ||||
| import ( | ||||
| 	"html/template" | ||||
| 	"path" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/logger" | ||||
| ) | ||||
|  | ||||
| func loadTemplates(dir string) *template.Template { | ||||
| 	if dir == "" { | ||||
| 		return getTemplates() | ||||
| 	} | ||||
| 	logger.Printf("using custom template directory %q", dir) | ||||
| 	funcMap := template.FuncMap{ | ||||
| 		"ToUpper": strings.ToUpper, | ||||
| 		"ToLower": strings.ToLower, | ||||
| 	} | ||||
| 	t, err := template.New("").Funcs(funcMap).ParseFiles(path.Join(dir, "sign_in.html"), path.Join(dir, "error.html")) | ||||
| 	if err != nil { | ||||
| 		logger.Fatalf("failed parsing template %s", err) | ||||
| 	} | ||||
| 	return t | ||||
| } | ||||
|  | ||||
| func getTemplates() *template.Template { | ||||
| 	t, err := template.New("foo").Parse(`{{define "sign_in.html"}} | ||||
| <!DOCTYPE html> | ||||
| <html lang="en" charset="utf-8"> | ||||
| <head> | ||||
| 	<title>Sign In</title> | ||||
| 	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | ||||
| 	<style> | ||||
| 	body { | ||||
| 		font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; | ||||
| 		font-size: 14px; | ||||
| 		line-height: 1.42857143; | ||||
| 		color: #333; | ||||
| 		background: #f0f0f0; | ||||
| 	} | ||||
| 	.signin { | ||||
| 		display:block; | ||||
| 		margin:20px auto; | ||||
| 		max-width:400px; | ||||
| 		background: #fff; | ||||
| 		border:1px solid #ccc; | ||||
| 		border-radius: 10px; | ||||
| 		padding: 20px; | ||||
| 	} | ||||
| 	.center { | ||||
| 		text-align:center; | ||||
| 	} | ||||
| 	.btn { | ||||
| 		color: #fff; | ||||
| 		background-color: #428bca; | ||||
| 		border: 1px solid #357ebd; | ||||
| 		-webkit-border-radius: 4; | ||||
| 		-moz-border-radius: 4; | ||||
| 		border-radius: 4px; | ||||
| 		font-size: 14px; | ||||
| 		padding: 6px 12px; | ||||
| 	  	text-decoration: none; | ||||
| 		cursor: pointer; | ||||
| 	} | ||||
|  | ||||
| 	.btn:hover { | ||||
| 		background-color: #3071a9; | ||||
| 		border-color: #285e8e; | ||||
| 		text-decoration: none; | ||||
| 	} | ||||
| 	label { | ||||
| 		display: inline-block; | ||||
| 		max-width: 100%; | ||||
| 		margin-bottom: 5px; | ||||
| 		font-weight: 700; | ||||
| 	} | ||||
| 	input { | ||||
| 		display: block; | ||||
| 		width: 100%; | ||||
| 		height: 34px; | ||||
| 		padding: 6px 12px; | ||||
| 		font-size: 14px; | ||||
| 		line-height: 1.42857143; | ||||
| 		color: #555; | ||||
| 		background-color: #fff; | ||||
| 		background-image: none; | ||||
| 		border: 1px solid #ccc; | ||||
| 		border-radius: 4px; | ||||
| 		-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); | ||||
| 		box-shadow: inset 0 1px 1px rgba(0,0,0,.075); | ||||
| 		-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; | ||||
| 		-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; | ||||
| 		transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; | ||||
| 		margin:0; | ||||
| 		box-sizing: border-box; | ||||
| 	} | ||||
| 	footer { | ||||
| 		display:block; | ||||
| 		font-size:10px; | ||||
| 		color:#aaa; | ||||
| 		text-align:center; | ||||
| 		margin-bottom:10px; | ||||
| 	} | ||||
| 	footer a { | ||||
| 		display:inline-block; | ||||
| 		height:25px; | ||||
| 		line-height:25px; | ||||
| 		color:#aaa; | ||||
| 		text-decoration:underline; | ||||
| 	} | ||||
| 	footer a:hover { | ||||
| 		color:#aaa; | ||||
| 	} | ||||
| 	</style> | ||||
| </head> | ||||
| <body> | ||||
| 	<div class="signin center"> | ||||
| 	<form method="GET" action="{{.ProxyPrefix}}/start"> | ||||
| 	<input type="hidden" name="rd" value="{{.Redirect}}"> | ||||
| 	{{ if .SignInMessage }} | ||||
| 	<p>{{.SignInMessage}}</p> | ||||
| 	{{ end}} | ||||
| 	<button type="submit" class="btn">Sign in with {{.ProviderName}}</button><br/> | ||||
| 	</form> | ||||
| 	</div> | ||||
|  | ||||
| 	{{ if .CustomLogin }} | ||||
| 	<div class="signin"> | ||||
| 	<form method="POST" action="{{.ProxyPrefix}}/sign_in"> | ||||
| 		<input type="hidden" name="rd" value="{{.Redirect}}"> | ||||
| 		<label for="username">Username:</label><input type="text" name="username" id="username" size="10"><br/> | ||||
| 		<label for="password">Password:</label><input type="password" name="password" id="password" size="10"><br/> | ||||
| 		<button type="submit" class="btn">Sign In</button> | ||||
| 	</form> | ||||
| 	</div> | ||||
| 	{{ end }} | ||||
| 	<script> | ||||
| 		if (window.location.hash) { | ||||
| 			(function() { | ||||
| 				var inputs = document.getElementsByName('rd'); | ||||
| 				for (var i = 0; i < inputs.length; i++) { | ||||
| 					// Add hash, but make sure it is only added once | ||||
| 					var idx = inputs[i].value.indexOf('#'); | ||||
| 					if (idx >= 0) { | ||||
| 						// Remove existing hash from URL | ||||
| 						inputs[i].value = inputs[i].value.substr(0, idx); | ||||
| 					} | ||||
| 					inputs[i].value += window.location.hash; | ||||
| 				} | ||||
| 			})(); | ||||
| 		} | ||||
| 	</script> | ||||
| 	<footer> | ||||
| 	{{ if eq .Footer "-" }} | ||||
| 	{{ else if eq .Footer ""}} | ||||
| 	Secured with <a href="https://github.com/oauth2-proxy/oauth2-proxy#oauth2_proxy">OAuth2 Proxy</a> version {{.Version}} | ||||
| 	{{ else }} | ||||
| 	{{.Footer}} | ||||
| 	{{ end }} | ||||
| 	</footer> | ||||
| </body> | ||||
| </html> | ||||
| {{end}}`) | ||||
| 	if err != nil { | ||||
| 		logger.Fatalf("failed parsing template %s", err) | ||||
| 	} | ||||
|  | ||||
| 	t, err = t.Parse(`{{define "error.html"}} | ||||
| <!DOCTYPE html> | ||||
| <html lang="en" charset="utf-8"> | ||||
| <head> | ||||
| 	<title>{{.Title}}</title> | ||||
| 	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | ||||
| </head> | ||||
| <body> | ||||
| 	<h2>{{.Title}}</h2> | ||||
| 	<p>{{.Message}}</p> | ||||
| 	<hr> | ||||
| 	<p><a href="{{.ProxyPrefix}}/sign_in">Sign In</a></p> | ||||
| </body> | ||||
| </html>{{end}}`) | ||||
| 	if err != nil { | ||||
| 		logger.Fatalf("failed parsing template %s", err) | ||||
| 	} | ||||
| 	return t | ||||
| } | ||||
							
								
								
									
										62
									
								
								proxy/pkg/proxy/templates_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								proxy/pkg/proxy/templates_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | ||||
| package proxy | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestLoadTemplates(t *testing.T) { | ||||
| 	data := struct { | ||||
| 		TestString string | ||||
| 	}{ | ||||
| 		TestString: "Testing", | ||||
| 	} | ||||
|  | ||||
| 	templates := loadTemplates("") | ||||
| 	assert.NotEqual(t, templates, nil) | ||||
|  | ||||
| 	var defaultSignin bytes.Buffer | ||||
| 	templates.ExecuteTemplate(&defaultSignin, "sign_in.html", data) | ||||
| 	assert.Equal(t, "\n<!DOCTYPE html>", defaultSignin.String()[0:16]) | ||||
|  | ||||
| 	var defaultError bytes.Buffer | ||||
| 	templates.ExecuteTemplate(&defaultError, "error.html", data) | ||||
| 	assert.Equal(t, "\n<!DOCTYPE html>", defaultError.String()[0:16]) | ||||
|  | ||||
| 	dir, err := ioutil.TempDir("", "templatetest") | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 	defer os.RemoveAll(dir) | ||||
|  | ||||
| 	templateHTML := `{{.TestString}} {{.TestString | ToLower}} {{.TestString | ToUpper}}` | ||||
| 	signInFile := filepath.Join(dir, "sign_in.html") | ||||
| 	if err := ioutil.WriteFile(signInFile, []byte(templateHTML), 0666); err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 	errorFile := filepath.Join(dir, "error.html") | ||||
| 	if err := ioutil.WriteFile(errorFile, []byte(templateHTML), 0666); err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 	templates = loadTemplates(dir) | ||||
| 	assert.NotEqual(t, templates, nil) | ||||
|  | ||||
| 	var sitpl bytes.Buffer | ||||
| 	templates.ExecuteTemplate(&sitpl, "sign_in.html", data) | ||||
| 	assert.Equal(t, "Testing testing TESTING", sitpl.String()) | ||||
|  | ||||
| 	var errtpl bytes.Buffer | ||||
| 	templates.ExecuteTemplate(&errtpl, "error.html", data) | ||||
| 	assert.Equal(t, "Testing testing TESTING", errtpl.String()) | ||||
| } | ||||
|  | ||||
| func TestTemplatesCompile(t *testing.T) { | ||||
| 	templates := getTemplates() | ||||
| 	assert.NotEqual(t, templates, nil) | ||||
| } | ||||
							
								
								
									
										212
									
								
								proxy/pkg/server/api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								proxy/pkg/server/api.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,212 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"crypto/sha512" | ||||
| 	"encoding/hex" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/BeryJu/passbook/proxy/pkg/client" | ||||
| 	"github.com/BeryJu/passbook/proxy/pkg/client/outposts" | ||||
| 	"github.com/getsentry/sentry-go" | ||||
| 	"github.com/go-openapi/runtime" | ||||
| 	"github.com/recws-org/recws" | ||||
|  | ||||
| 	httptransport "github.com/go-openapi/runtime/client" | ||||
| 	"github.com/go-openapi/strfmt" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| const ConfigLogLevel = "log_level" | ||||
| const ConfigErrorReportingEnabled = "error_reporting_enabled" | ||||
| const ConfigErrorReportingEnvironment = "error_reporting_environment" | ||||
|  | ||||
| // APIController main controller which connects to the passbook api via http and ws | ||||
| type APIController struct { | ||||
| 	client *client.Passbook | ||||
| 	auth   runtime.ClientAuthInfoWriter | ||||
| 	token  string | ||||
|  | ||||
| 	server *Server | ||||
|  | ||||
| 	commonOpts *options.Options | ||||
|  | ||||
| 	lastBundleHash string | ||||
| 	logger         *log.Entry | ||||
|  | ||||
| 	wsConn recws.RecConn | ||||
| } | ||||
|  | ||||
| func getCommonOptions() *options.Options { | ||||
| 	commonOpts := options.NewOptions() | ||||
| 	commonOpts.Cookie.Name = "passbook_proxy" | ||||
| 	commonOpts.EmailDomains = []string{"*"} | ||||
| 	commonOpts.ProviderType = "oidc" | ||||
| 	commonOpts.ProxyPrefix = "/pbprox" | ||||
| 	commonOpts.SkipProviderButton = true | ||||
| 	commonOpts.Logging.SilencePing = true | ||||
| 	commonOpts.SetXAuthRequest = true | ||||
| 	commonOpts.SetAuthorization = true | ||||
| 	return commonOpts | ||||
| } | ||||
|  | ||||
| func doGlobalSetup(config map[string]interface{}) { | ||||
| 	switch config[ConfigLogLevel].(string) { | ||||
| 	case "debug": | ||||
| 		log.SetLevel(log.DebugLevel) | ||||
| 	case "info": | ||||
| 		log.SetLevel(log.InfoLevel) | ||||
| 	case "warning": | ||||
| 		log.SetLevel(log.WarnLevel) | ||||
| 	case "error": | ||||
| 		log.SetLevel(log.ErrorLevel) | ||||
| 	default: | ||||
| 		log.SetLevel(log.DebugLevel) | ||||
| 	} | ||||
|  | ||||
| 	var dsn string | ||||
| 	if config[ConfigErrorReportingEnabled].(bool) { | ||||
| 		dsn = "https://33cdbcb23f8b436dbe0ee06847410b67@sentry.beryju.org/3" | ||||
| 		log.Debug("Error reporting enabled") | ||||
| 	} | ||||
|  | ||||
| 	err := sentry.Init(sentry.ClientOptions{ | ||||
| 		Dsn:         dsn, | ||||
| 		Environment: config[ConfigErrorReportingEnvironment].(string), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("sentry.Init: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	defer sentry.Flush(2 * time.Second) | ||||
| } | ||||
|  | ||||
| func getTLSTransport() http.RoundTripper { | ||||
| 	_, set := os.LookupEnv("PASSBOOK_INSECURE") | ||||
| 	tlsTransport, err := httptransport.TLSTransport(httptransport.TLSClientOptions{ | ||||
| 		InsecureSkipVerify: set, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	return tlsTransport | ||||
| } | ||||
|  | ||||
| // NewAPIController initialise new API Controller instance from URL and API token | ||||
| func NewAPIController(pbURL url.URL, token string) *APIController { | ||||
| 	transport := httptransport.New(pbURL.Host, client.DefaultBasePath, []string{pbURL.Scheme}) | ||||
|  | ||||
| 	transport.Transport = getTLSTransport() | ||||
|  | ||||
| 	// create the transport | ||||
| 	auth := httptransport.BasicAuth("", token) | ||||
|  | ||||
| 	// create the API client, with the transport | ||||
| 	apiClient := client.New(transport, strfmt.Default) | ||||
|  | ||||
| 	// Because we don't know the outpost UUID, we simply do a list and pick the first | ||||
| 	// The service account this token belongs to should only have access to a single outpost | ||||
| 	outposts, err := apiClient.Outposts.OutpostsOutpostsList(outposts.NewOutpostsOutpostsListParams(), auth) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	outpost := outposts.Payload.Results[0] | ||||
| 	doGlobalSetup(outpost.Config.(map[string]interface{})) | ||||
|  | ||||
| 	ac := &APIController{ | ||||
| 		client: apiClient, | ||||
| 		auth:   auth, | ||||
| 		token:  token, | ||||
|  | ||||
| 		logger:     log.WithField("component", "api-controller"), | ||||
| 		commonOpts: getCommonOptions(), | ||||
| 		server:     NewServer(), | ||||
|  | ||||
| 		lastBundleHash: "", | ||||
| 	} | ||||
| 	ac.initWS(pbURL, outpost.Pk) | ||||
| 	return ac | ||||
| } | ||||
|  | ||||
| func (a *APIController) bundleProviders() ([]*providerBundle, error) { | ||||
| 	providers, err := a.client.Outposts.OutpostsProxyList(outposts.NewOutpostsProxyListParams(), a.auth) | ||||
| 	if err != nil { | ||||
| 		a.logger.WithError(err).Error("Failed to fetch providers") | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	// Check provider hash to see if anything is changed | ||||
| 	hasher := sha512.New() | ||||
| 	bin, _ := providers.Payload.MarshalBinary() | ||||
| 	hash := hex.EncodeToString(hasher.Sum(bin)) | ||||
| 	if hash == a.lastBundleHash { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	a.lastBundleHash = hash | ||||
|  | ||||
| 	bundles := make([]*providerBundle, len(providers.Payload.Results)) | ||||
|  | ||||
| 	for idx, provider := range providers.Payload.Results { | ||||
| 		externalHost, err := url.Parse(*provider.ExternalHost) | ||||
| 		if err != nil { | ||||
| 			log.WithError(err).Warning("Failed to parse URL, skipping provider") | ||||
| 		} | ||||
| 		bundles[idx] = &providerBundle{ | ||||
| 			a:    a, | ||||
| 			Host: externalHost.Hostname(), | ||||
| 		} | ||||
| 		bundles[idx].Build(provider) | ||||
| 	} | ||||
| 	return bundles, nil | ||||
| } | ||||
|  | ||||
| func (a *APIController) updateHTTPServer(bundles []*providerBundle) { | ||||
| 	newMap := make(map[string]*providerBundle) | ||||
| 	for _, bundle := range bundles { | ||||
| 		newMap[bundle.Host] = bundle | ||||
| 	} | ||||
| 	a.logger.Debug("Swapped maps") | ||||
| 	a.server.Handlers = newMap | ||||
| } | ||||
|  | ||||
| // UpdateIfRequired Updates the HTTP Server config if required, automatically swaps the handlers | ||||
| func (a *APIController) UpdateIfRequired() error { | ||||
| 	bundles, err := a.bundleProviders() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if bundles == nil { | ||||
| 		a.logger.Debug("Providers have not changed, not updating") | ||||
| 		return nil | ||||
| 	} | ||||
| 	a.updateHTTPServer(bundles) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Start Starts all handlers, non-blocking | ||||
| func (a *APIController) Start() error { | ||||
| 	err := a.UpdateIfRequired() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	go func() { | ||||
| 		a.logger.Debug("Starting HTTP Server...") | ||||
| 		a.server.ServeHTTP() | ||||
| 	}() | ||||
| 	go func() { | ||||
| 		a.logger.Debug("Starting HTTPs Server...") | ||||
| 		a.server.ServeHTTPS() | ||||
| 	}() | ||||
| 	go func() { | ||||
| 		a.logger.Debug("Starting WS Handler...") | ||||
| 		a.startWSHandler() | ||||
| 	}() | ||||
| 	go func() { | ||||
| 		a.logger.Debug("Starting WS Health notifier...") | ||||
| 		a.startWSHealth() | ||||
| 	}() | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										123
									
								
								proxy/pkg/server/api_bundle.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								proxy/pkg/server/api_bundle.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,123 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
|  | ||||
| 	"github.com/BeryJu/passbook/proxy/pkg/client/crypto" | ||||
| 	"github.com/BeryJu/passbook/proxy/pkg/models" | ||||
| 	"github.com/BeryJu/passbook/proxy/pkg/proxy" | ||||
| 	"github.com/jinzhu/copier" | ||||
| 	"github.com/justinas/alice" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/middleware" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/validation" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| type providerBundle struct { | ||||
| 	http.Handler | ||||
|  | ||||
| 	a     *APIController | ||||
| 	proxy *proxy.OAuthProxy | ||||
| 	Host  string | ||||
|  | ||||
| 	cert *tls.Certificate | ||||
| } | ||||
|  | ||||
| func (pb *providerBundle) prepareOpts(provider *models.ProxyOutpostConfig) *options.Options { | ||||
| 	externalHost, err := url.Parse(*provider.ExternalHost) | ||||
| 	if err != nil { | ||||
| 		log.WithError(err).Warning("Failed to parse URL, skipping provider") | ||||
| 		return nil | ||||
| 	} | ||||
| 	providerOpts := &options.Options{} | ||||
| 	copier.Copy(&providerOpts, &pb.a.commonOpts) | ||||
| 	providerOpts.ClientID = provider.ClientID | ||||
| 	providerOpts.ClientSecret = provider.ClientSecret | ||||
|  | ||||
| 	providerOpts.Cookie.Secret = provider.CookieSecret | ||||
| 	providerOpts.Cookie.Secure = externalHost.Scheme == "https" | ||||
|  | ||||
| 	providerOpts.SkipOIDCDiscovery = true | ||||
| 	providerOpts.OIDCIssuerURL = *provider.OidcConfiguration.Issuer | ||||
| 	providerOpts.LoginURL = *provider.OidcConfiguration.AuthorizationEndpoint | ||||
| 	providerOpts.RedeemURL = *provider.OidcConfiguration.TokenEndpoint | ||||
| 	providerOpts.OIDCJwksURL = *provider.OidcConfiguration.JwksURI | ||||
| 	providerOpts.ProfileURL = *provider.OidcConfiguration.UserinfoEndpoint | ||||
|  | ||||
| 	providerOpts.UpstreamServers = []options.Upstream{ | ||||
| 		{ | ||||
| 			ID:   "default", | ||||
| 			URI:  *provider.InternalHost, | ||||
| 			Path: "/", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	if provider.Certificate != nil { | ||||
| 		pb.a.logger.WithField("provider", provider.ClientID).Debug("Enabling TLS") | ||||
| 		cert, err := pb.a.client.Crypto.CryptoCertificatekeypairsRead(&crypto.CryptoCertificatekeypairsReadParams{ | ||||
| 			Context: context.Background(), | ||||
| 			KpUUID:  *provider.Certificate, | ||||
| 		}, pb.a.auth) | ||||
| 		if err != nil { | ||||
| 			pb.a.logger.WithField("provider", provider.ClientID).WithError(err).Warning("Failed to fetch certificate") | ||||
| 			return providerOpts | ||||
| 		} | ||||
| 		x509cert, err := tls.X509KeyPair([]byte(*cert.Payload.CertificateData), []byte(cert.Payload.KeyData)) | ||||
| 		if err != nil { | ||||
| 			pb.a.logger.WithField("provider", provider.ClientID).WithError(err).Warning("Failed to parse certificate") | ||||
| 			return providerOpts | ||||
| 		} | ||||
| 		pb.cert = &x509cert | ||||
| 		pb.a.logger.WithField("provider", provider.ClientID).WithField("certificate-key-pair", *cert.Payload.Name).Debug("Loaded certificates") | ||||
| 	} | ||||
| 	return providerOpts | ||||
| } | ||||
|  | ||||
| func (pb *providerBundle) Build(provider *models.ProxyOutpostConfig) { | ||||
| 	opts := pb.prepareOpts(provider) | ||||
|  | ||||
| 	chain := alice.New() | ||||
|  | ||||
| 	if opts.ForceHTTPS { | ||||
| 		_, httpsPort, err := net.SplitHostPort(opts.HTTPSAddress) | ||||
| 		if err != nil { | ||||
| 			log.Fatalf("FATAL: invalid HTTPS address %q: %v", opts.HTTPAddress, err) | ||||
| 		} | ||||
| 		chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort)) | ||||
| 	} | ||||
|  | ||||
| 	healthCheckPaths := []string{opts.PingPath} | ||||
| 	healthCheckUserAgents := []string{opts.PingUserAgent} | ||||
| 	if opts.GCPHealthChecks { | ||||
| 		healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check") | ||||
| 		healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0") | ||||
| 	} | ||||
|  | ||||
| 	// To silence logging of health checks, register the health check handler before | ||||
| 	// the logging handler | ||||
| 	if opts.Logging.SilencePing { | ||||
| 		chain = chain.Append(middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents), LoggingHandler) | ||||
| 	} else { | ||||
| 		chain = chain.Append(LoggingHandler, middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents)) | ||||
| 	} | ||||
|  | ||||
| 	err := validation.Validate(opts) | ||||
| 	if err != nil { | ||||
| 		log.Printf("%s", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	oauthproxy, err := proxy.NewOAuthProxy(opts) | ||||
| 	if err != nil { | ||||
| 		log.Errorf("ERROR: Failed to initialise OAuth2 Proxy: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| 	pb.proxy = oauthproxy | ||||
| 	pb.Handler = chain.Then(oauthproxy) | ||||
| } | ||||
							
								
								
									
										85
									
								
								proxy/pkg/server/api_ws.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								proxy/pkg/server/api_ws.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,85 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/go-openapi/strfmt" | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	"github.com/recws-org/recws" | ||||
| ) | ||||
|  | ||||
| func (ac *APIController) initWS(pbURL url.URL, outpostUUID strfmt.UUID) { | ||||
| 	pathTemplate := "%s://%s/ws/outpost/%s/" | ||||
| 	scheme := strings.ReplaceAll(pbURL.Scheme, "http", "ws") | ||||
|  | ||||
| 	header := http.Header{ | ||||
| 		"Authorization": []string{ac.token}, | ||||
| 	} | ||||
|  | ||||
| 	_, set := os.LookupEnv("PASSBOOK_INSECURE") | ||||
|  | ||||
| 	ws := recws.RecConn{ | ||||
| 		// KeepAliveTimeout: 10 * time.Second, | ||||
| 		NonVerbose: true, | ||||
| 		TLSClientConfig: &tls.Config{ | ||||
| 			InsecureSkipVerify: set, | ||||
| 		}, | ||||
| 	} | ||||
| 	ws.Dial(fmt.Sprintf(pathTemplate, scheme, pbURL.Host, outpostUUID.String()), header) | ||||
|  | ||||
| 	ac.logger.WithField("outpost", outpostUUID.String()).Debug("connecting to passbook") | ||||
|  | ||||
| 	ac.wsConn = ws | ||||
| } | ||||
|  | ||||
| // Shutdown Gracefully stops all workers, disconnects from websocket | ||||
| func (ac *APIController) Shutdown() { | ||||
| 	// Cleanly close the connection by sending a close message and then | ||||
| 	// waiting (with timeout) for the server to close the connection. | ||||
| 	err := ac.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) | ||||
| 	if err != nil { | ||||
| 		ac.logger.Println("write close:", err) | ||||
| 		return | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (ac *APIController) startWSHandler() { | ||||
| 	for { | ||||
| 		var wsMsg websocketMessage | ||||
| 		err := ac.wsConn.ReadJSON(&wsMsg) | ||||
| 		if err != nil { | ||||
| 			ac.logger.Println("read:", err) | ||||
| 			return | ||||
| 		} | ||||
| 		if wsMsg.Instruction != WebsocketInstructionAck { | ||||
| 			ac.logger.Debugf("%+v\n", wsMsg) | ||||
| 		} | ||||
| 		if wsMsg.Instruction == WebsocketInstructionTriggerUpdate { | ||||
| 			err := ac.UpdateIfRequired() | ||||
| 			if err != nil { | ||||
| 				ac.logger.WithError(err).Debug("Failed to update") | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (ac *APIController) startWSHealth() { | ||||
| 	for ; true; <-time.Tick(time.Second * 10) { | ||||
| 		aliveMsg := websocketMessage{ | ||||
| 			Instruction: WebsocketInstructionHello, | ||||
| 			Args:        make(map[string]interface{}), | ||||
| 		} | ||||
| 		err := ac.wsConn.WriteJSON(aliveMsg) | ||||
| 		if err != nil { | ||||
| 			ac.logger.Println("write:", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										17
									
								
								proxy/pkg/server/api_ws_msg.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								proxy/pkg/server/api_ws_msg.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| package server | ||||
|  | ||||
| type websocketInstruction int | ||||
|  | ||||
| const ( | ||||
| 	// WebsocketInstructionAck Code used to acknowledge a previous message | ||||
| 	WebsocketInstructionAck websocketInstruction = 0 | ||||
| 	// WebsocketInstructionHello Code used to send a healthcheck keepalive | ||||
| 	WebsocketInstructionHello websocketInstruction = 1 | ||||
| 	// WebsocketInstructionTriggerUpdate Code received to trigger a config update | ||||
| 	WebsocketInstructionTriggerUpdate websocketInstruction = 2 | ||||
| ) | ||||
|  | ||||
| type websocketMessage struct { | ||||
| 	Instruction websocketInstruction   `json:"instruction"` | ||||
| 	Args        map[string]interface{} `json:"args"` | ||||
| } | ||||
							
								
								
									
										63
									
								
								proxy/pkg/server/cert.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								proxy/pkg/server/cert.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"crypto/x509/pkix" | ||||
| 	"encoding/pem" | ||||
| 	"math/big" | ||||
| 	"time" | ||||
|  | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| func generateSelfSignedCert() (tls.Certificate, error) { | ||||
|  | ||||
| 	priv, err := rsa.GenerateKey(rand.Reader, 2048) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("Failed to generate private key: %v", err) | ||||
| 		return tls.Certificate{}, err | ||||
| 	} | ||||
|  | ||||
| 	keyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | ||||
|  | ||||
| 	notBefore := time.Now() | ||||
| 	notAfter := notBefore.Add(365 * 24 * time.Hour) | ||||
|  | ||||
| 	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) | ||||
| 	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("Failed to generate serial number: %v", err) | ||||
| 		return tls.Certificate{}, err | ||||
| 	} | ||||
|  | ||||
| 	template := x509.Certificate{ | ||||
| 		SerialNumber: serialNumber, | ||||
| 		Subject: pkix.Name{ | ||||
| 			Organization: []string{"passbook"}, | ||||
| 			CommonName:   "passbook Proxy default certificate", | ||||
| 		}, | ||||
| 		NotBefore: notBefore, | ||||
| 		NotAfter:  notAfter, | ||||
|  | ||||
| 		KeyUsage:              keyUsage, | ||||
| 		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | ||||
| 		BasicConstraintsValid: true, | ||||
| 	} | ||||
|  | ||||
| 	template.DNSNames = []string{"*"} | ||||
|  | ||||
| 	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) | ||||
| 	if err != nil { | ||||
| 		log.Warning(err) | ||||
| 	} | ||||
| 	pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) | ||||
| 	privBytes, err := x509.MarshalPKCS8PrivateKey(priv) | ||||
| 	if err != nil { | ||||
| 		log.Warning(err) | ||||
| 	} | ||||
| 	privPemByes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) | ||||
| 	return tls.X509KeyPair(pemBytes, privPemByes) | ||||
| } | ||||
							
								
								
									
										123
									
								
								proxy/pkg/server/middleware.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								proxy/pkg/server/middleware.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,123 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/pkg/logger" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| // responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP status | ||||
| // code and body size | ||||
| type responseLogger struct { | ||||
| 	w        http.ResponseWriter | ||||
| 	status   int | ||||
| 	size     int | ||||
| 	upstream string | ||||
| 	authInfo string | ||||
| } | ||||
|  | ||||
| // Header returns the ResponseWriter's Header | ||||
| func (l *responseLogger) Header() http.Header { | ||||
| 	return l.w.Header() | ||||
| } | ||||
|  | ||||
| // Support Websocket | ||||
| func (l *responseLogger) Hijack() (rwc net.Conn, buf *bufio.ReadWriter, err error) { | ||||
| 	if hj, ok := l.w.(http.Hijacker); ok { | ||||
| 		return hj.Hijack() | ||||
| 	} | ||||
| 	return nil, nil, errors.New("http.Hijacker is not available on writer") | ||||
| } | ||||
|  | ||||
| // ExtractGAPMetadata extracts and removes GAP headers from the ResponseWriter's | ||||
| // Header | ||||
| func (l *responseLogger) ExtractGAPMetadata() { | ||||
| 	upstream := l.w.Header().Get("GAP-Upstream-Address") | ||||
| 	if upstream != "" { | ||||
| 		l.upstream = upstream | ||||
| 		l.w.Header().Del("GAP-Upstream-Address") | ||||
| 	} | ||||
| 	authInfo := l.w.Header().Get("GAP-Auth") | ||||
| 	if authInfo != "" { | ||||
| 		l.authInfo = authInfo | ||||
| 		l.w.Header().Del("GAP-Auth") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Write writes the response using the ResponseWriter | ||||
| func (l *responseLogger) Write(b []byte) (int, error) { | ||||
| 	if l.status == 0 { | ||||
| 		// The status will be StatusOK if WriteHeader has not been called yet | ||||
| 		l.status = http.StatusOK | ||||
| 	} | ||||
| 	l.ExtractGAPMetadata() | ||||
| 	size, err := l.w.Write(b) | ||||
| 	l.size += size | ||||
| 	return size, err | ||||
| } | ||||
|  | ||||
| // WriteHeader writes the status code for the Response | ||||
| func (l *responseLogger) WriteHeader(s int) { | ||||
| 	l.ExtractGAPMetadata() | ||||
| 	l.w.WriteHeader(s) | ||||
| 	l.status = s | ||||
| } | ||||
|  | ||||
| // Status returns the response status code | ||||
| func (l *responseLogger) Status() int { | ||||
| 	return l.status | ||||
| } | ||||
|  | ||||
| // Size returns the response size | ||||
| func (l *responseLogger) Size() int { | ||||
| 	return l.size | ||||
| } | ||||
|  | ||||
| // Flush sends any buffered data to the client | ||||
| func (l *responseLogger) Flush() { | ||||
| 	if flusher, ok := l.w.(http.Flusher); ok { | ||||
| 		flusher.Flush() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // loggingHandler is the http.Handler implementation for LoggingHandler | ||||
| type loggingHandler struct { | ||||
| 	handler http.Handler | ||||
| 	logger  *log.Entry | ||||
| } | ||||
|  | ||||
| // LoggingHandler provides an http.Handler which logs requests to the HTTP server | ||||
| func LoggingHandler(h http.Handler) http.Handler { | ||||
| 	return loggingHandler{ | ||||
| 		handler: h, | ||||
| 		logger:  log.WithField("component", "http-server"), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||
| 	t := time.Now() | ||||
| 	url := *req.URL | ||||
| 	responseLogger := &responseLogger{w: w} | ||||
| 	h.handler.ServeHTTP(responseLogger, req) | ||||
| 	duration := float64(time.Since(t)) / float64(time.Second) | ||||
| 	h.logger.WithFields(log.Fields{ | ||||
| 		"Client":          req.RemoteAddr, | ||||
| 		"Host":            req.Host, | ||||
| 		"Protocol":        req.Proto, | ||||
| 		"RequestDuration": fmt.Sprintf("%0.3f", duration), | ||||
| 		"RequestMethod":   req.Method, | ||||
| 		"ResponseSize":    responseLogger.Size(), | ||||
| 		"StatusCode":      responseLogger.Status(), | ||||
| 		"Timestamp":       logger.FormatTimestamp(t), | ||||
| 		"Upstream":        responseLogger.upstream, | ||||
| 		"UserAgent":       req.UserAgent(), | ||||
| 		"Username":        responseLogger.authInfo, | ||||
| 	}).Info(url.RequestURI()) | ||||
| 	// logger.PrintReq(responseLogger.authInfo, responseLogger.upstream, req, url, t, , ) | ||||
| } | ||||
							
								
								
									
										152
									
								
								proxy/pkg/server/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								proxy/pkg/server/server.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,152 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"errors" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	log "github.com/sirupsen/logrus" | ||||
|  | ||||
| 	sentryhttp "github.com/getsentry/sentry-go/http" | ||||
| ) | ||||
|  | ||||
| // Server represents an HTTP server | ||||
| type Server struct { | ||||
| 	Handlers map[string]*providerBundle | ||||
|  | ||||
| 	stop   chan struct{} // channel for waiting shutdown | ||||
| 	logger *log.Entry | ||||
|  | ||||
| 	defaultCert tls.Certificate | ||||
| } | ||||
|  | ||||
| // NewServer initialise a new HTTP Server | ||||
| func NewServer() *Server { | ||||
| 	defaultCert, err := generateSelfSignedCert() | ||||
| 	if err != nil { | ||||
| 		log.Warning(err) | ||||
| 	} | ||||
| 	return &Server{ | ||||
| 		Handlers:    make(map[string]*providerBundle), | ||||
| 		logger:      log.WithField("component", "http-server"), | ||||
| 		defaultCert: defaultCert, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // ServeHTTP constructs a net.Listener and starts handling HTTP requests | ||||
| func (s *Server) ServeHTTP() { | ||||
| 	// TODO: make this a setting | ||||
| 	listenAddress := "localhost:4180" | ||||
| 	listener, err := net.Listen("tcp", listenAddress) | ||||
| 	if err != nil { | ||||
| 		s.logger.Fatalf("FATAL: listen (%s) failed - %s", listenAddress, err) | ||||
| 	} | ||||
| 	s.logger.Printf("listening on %s", listener.Addr()) | ||||
| 	s.serve(listener) | ||||
| 	s.logger.Printf("closing %s", listener.Addr()) | ||||
| } | ||||
|  | ||||
| func (s *Server) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) { | ||||
| 	handler, ok := s.Handlers[info.ServerName] | ||||
| 	if !ok { | ||||
| 		s.logger.WithField("server-name", info.ServerName).Debug("Handler does not exist") | ||||
| 		return &s.defaultCert, nil | ||||
| 	} | ||||
| 	if handler.cert == nil { | ||||
| 		s.logger.WithField("server-name", info.ServerName).Debug("Handler does not have a certificate") | ||||
| 		return &s.defaultCert, nil | ||||
| 	} | ||||
| 	return handler.cert, nil | ||||
| } | ||||
|  | ||||
| // ServeHTTPS constructs a net.Listener and starts handling HTTPS requests | ||||
| func (s *Server) ServeHTTPS() { | ||||
| 	// TODO: make this a setting | ||||
| 	listenAddress := "localhost:4443" | ||||
| 	config := &tls.Config{ | ||||
| 		MinVersion:     tls.VersionTLS12, | ||||
| 		MaxVersion:     tls.VersionTLS12, | ||||
| 		GetCertificate: s.getCertificates, | ||||
| 	} | ||||
|  | ||||
| 	ln, err := net.Listen("tcp", listenAddress) | ||||
| 	if err != nil { | ||||
| 		s.logger.Fatalf("FATAL: listen (%s) failed - %s", listenAddress, err) | ||||
| 	} | ||||
| 	s.logger.Printf("listening on %s", ln.Addr()) | ||||
|  | ||||
| 	tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config) | ||||
| 	s.serve(tlsListener) | ||||
| 	s.logger.Printf("closing %s", tlsListener.Addr()) | ||||
| } | ||||
|  | ||||
| func (s *Server) handler(w http.ResponseWriter, r *http.Request) { | ||||
| 	handler, ok := s.Handlers[r.Host] | ||||
| 	if !ok { | ||||
| 		// If we only have one handler, host name switching doesn't matter | ||||
| 		if len(s.Handlers) == 1 { | ||||
| 			for k := range s.Handlers { | ||||
| 				s.Handlers[k].ServeHTTP(w, r) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 		s.logger.WithField("host", r.Host).Debug("Host header does not match any we know of") | ||||
| 		s.logger.Printf("%v+\n", s.Handlers) | ||||
| 		w.WriteHeader(400) | ||||
| 		return | ||||
| 	} | ||||
| 	s.logger.WithField("host", r.Host).Debug("passing request from host head") | ||||
| 	handler.ServeHTTP(w, r) | ||||
| } | ||||
|  | ||||
| func (s *Server) serve(listener net.Listener) { | ||||
| 	sentryHandler := sentryhttp.New(sentryhttp.Options{}) | ||||
|  | ||||
| 	srv := &http.Server{Handler: sentryHandler.HandleFunc(s.handler)} | ||||
|  | ||||
| 	// See https://golang.org/pkg/net/http/#Server.Shutdown | ||||
| 	idleConnsClosed := make(chan struct{}) | ||||
| 	go func() { | ||||
| 		<-s.stop // wait notification for stopping server | ||||
|  | ||||
| 		// We received an interrupt signal, shut down. | ||||
| 		if err := srv.Shutdown(context.Background()); err != nil { | ||||
| 			// Error from closing listeners, or context timeout: | ||||
| 			s.logger.Printf("HTTP server Shutdown: %v", err) | ||||
| 		} | ||||
| 		close(idleConnsClosed) | ||||
| 	}() | ||||
|  | ||||
| 	err := srv.Serve(listener) | ||||
| 	if err != nil && !errors.Is(err, http.ErrServerClosed) { | ||||
| 		s.logger.Errorf("ERROR: http.Serve() - %s", err) | ||||
| 	} | ||||
| 	<-idleConnsClosed | ||||
| } | ||||
|  | ||||
| // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted | ||||
| // connections. It's used by ListenAndServe and ListenAndServeTLS so | ||||
| // dead TCP connections (e.g. closing laptop mid-download) eventually | ||||
| // go away. | ||||
| type tcpKeepAliveListener struct { | ||||
| 	*net.TCPListener | ||||
| } | ||||
|  | ||||
| func (ln tcpKeepAliveListener) Accept() (net.Conn, error) { | ||||
| 	tc, err := ln.AcceptTCP() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	err = tc.SetKeepAlive(true) | ||||
| 	if err != nil { | ||||
| 		log.Printf("Error setting Keep-Alive: %v", err) | ||||
| 	} | ||||
| 	err = tc.SetKeepAlivePeriod(3 * time.Minute) | ||||
| 	if err != nil { | ||||
| 		log.Printf("Error setting Keep-Alive period: %v", err) | ||||
| 	} | ||||
| 	return tc, nil | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L