Proxy v2 (#189)
This commit is contained in:
		| @ -1,7 +1,7 @@ | |||||||
| [run] | [run] | ||||||
| source = passbook | source = passbook | ||||||
| omit = | omit = | ||||||
|     */wsgi.py |     */asgi.py | ||||||
|     manage.py |     manage.py | ||||||
|     */migrations/* |     */migrations/* | ||||||
|     */apps.py |     */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 |         run: docker push beryju/passbook:0.9.0-stable | ||||||
|       - name: Push Docker Container to Registry (latest) |       - name: Push Docker Container to Registry (latest) | ||||||
|         run: docker push beryju/passbook:latest |         run: docker push beryju/passbook:latest | ||||||
|   build-gatekeeper: |   build-proxy: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v1 |       - uses: actions/checkout@v1 | ||||||
| @ -34,16 +34,16 @@ jobs: | |||||||
|         run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD |         run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD | ||||||
|       - name: Building Docker Image |       - name: Building Docker Image | ||||||
|         run: | |         run: | | ||||||
|           cd gatekeeper |           cd proxy | ||||||
|           docker build \ |           docker build \ | ||||||
|           --no-cache \ |           --no-cache \ | ||||||
|           -t beryju/passbook-gatekeeper:0.9.0-stable \ |           -t beryju/passbook-proxy:0.9.0-stable \ | ||||||
|           -t beryju/passbook-gatekeeper:latest \ |           -t beryju/passbook-proxy:latest \ | ||||||
|           -f Dockerfile . |           -f Dockerfile . | ||||||
|       - name: Push Docker Container to Registry (versioned) |       - 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) |       - name: Push Docker Container to Registry (latest) | ||||||
|         run: docker push beryju/passbook-gatekeeper:latest |         run: docker push beryju/passbook-proxy:latest | ||||||
|   build-static: |   build-static: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     services: |     services: | ||||||
|  | |||||||
| @ -17,14 +17,16 @@ COPY --from=locker /app/requirements-dev.txt /app/ | |||||||
| WORKDIR /app/ | WORKDIR /app/ | ||||||
|  |  | ||||||
| RUN apt-get update && \ | 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/ && \ |     rm -rf /var/lib/apt/ && \ | ||||||
|     pip install -r requirements.txt  --no-cache-dir && \ |     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 |     adduser --system --no-create-home --uid 1000 --group --home /app passbook | ||||||
|  |  | ||||||
| COPY ./passbook/ /app/passbook | COPY ./passbook/ /app/passbook | ||||||
| COPY ./manage.py /app/ | COPY ./manage.py /app/ | ||||||
| COPY ./docker/uwsgi.ini /app/ | COPY ./docker/gunicorn.conf.py /app/ | ||||||
| COPY ./docker/bootstrap.sh /bootstrap.sh | COPY ./docker/bootstrap.sh /bootstrap.sh | ||||||
| COPY ./docker/wait_for_db.py /app/wait_for_db.py | 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 = "*" | psycopg2-binary = "*" | ||||||
| pycryptodome = "*" | pycryptodome = "*" | ||||||
| pyjwkest = "*" | pyjwkest = "*" | ||||||
| pyuwsgi = "*" | uvicorn = "*" | ||||||
|  | gunicorn = "*" | ||||||
| pyyaml = "*" | pyyaml = "*" | ||||||
| qrcode = "*" | qrcode = "*" | ||||||
| requests-oauthlib = "*" | requests-oauthlib = "*" | ||||||
| @ -39,6 +40,9 @@ structlog = "*" | |||||||
| swagger-spec-validator = "*" | swagger-spec-validator = "*" | ||||||
| urllib3 = {extras = ["secure"],version = "*"} | urllib3 = {extras = ["secure"],version = "*"} | ||||||
| dacite = "*" | dacite = "*" | ||||||
|  | channels = "*" | ||||||
|  | channels-redis = "*" | ||||||
|  | kubernetes = "*" | ||||||
|  |  | ||||||
| [requires] | [requires] | ||||||
| python_version = "3.8" | python_version = "3.8" | ||||||
|  | |||||||
							
								
								
									
										488
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										488
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "_meta": { |     "_meta": { | ||||||
|         "hash": { |         "hash": { | ||||||
|             "sha256": "8f099b73d5993a0693261bf3d2b0e696d4f4d7ddd69a10d3db8ffe59a8ebd805" |             "sha256": "a798bbd0b97857cac136c1743b8d6ad8bf8c3d95e2760c71d324bb2a7f47f678" | ||||||
|         }, |         }, | ||||||
|         "pipfile-spec": 6, |         "pipfile-spec": 6, | ||||||
|         "requires": { |         "requires": { | ||||||
| @ -16,11 +16,19 @@ | |||||||
|         ] |         ] | ||||||
|     }, |     }, | ||||||
|     "default": { |     "default": { | ||||||
|  |         "aioredis": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a", | ||||||
|  |                 "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3" | ||||||
|  |             ], | ||||||
|  |             "version": "==1.3.1" | ||||||
|  |         }, | ||||||
|         "amqp": { |         "amqp": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21", |                 "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21", | ||||||
|                 "sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59" |                 "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" |             "version": "==2.6.1" | ||||||
|         }, |         }, | ||||||
|         "asgiref": { |         "asgiref": { | ||||||
| @ -28,15 +36,40 @@ | |||||||
|                 "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", |                 "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", | ||||||
|                 "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" |                 "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|             "version": "==3.2.10" |             "version": "==3.2.10" | ||||||
|         }, |         }, | ||||||
|  |         "async-timeout": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", | ||||||
|  |                 "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_full_version >= '3.5.3'", | ||||||
|  |             "version": "==3.0.1" | ||||||
|  |         }, | ||||||
|         "attrs": { |         "attrs": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", |                 "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", | ||||||
|                 "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" |                 "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==20.1.0" |             "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": { |         "billiard": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede", |                 "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede", | ||||||
| @ -46,17 +79,26 @@ | |||||||
|         }, |         }, | ||||||
|         "boto3": { |         "boto3": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:b240ac281de363e25a8e1a4c862559d6a056d98dcb9f487fc94d73c6f6599dfc" |                 "sha256:4196b418598851ffd10cf9d1606694673cbfeca4ddf8b25d4e50addbd2fc60bf", | ||||||
|  |                 "sha256:69ad8f2184979e223e12ee3071674fdf910983cf9f4d6f34f7ec407b089064b5" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.14.53" |             "version": "==1.14.54" | ||||||
|         }, |         }, | ||||||
|         "botocore": { |         "botocore": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7e0272ceeb7747ed259a392e8d7b624cfd037085a8c59ef2b9f8916e7c556267", |                 "sha256:6fe05837646447d61acdaf1e3401b92cd9309f00b19c577a50d0ade7735a3403", | ||||||
|                 "sha256:d37a83ac23257c85c48b74ab81173980234f8fc078e7a9d312d0ee7d057f90e6" |                 "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": { |         "celery": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -106,6 +148,22 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==1.14.2" |             "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": { |         "chardet": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", |                 "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", | ||||||
| @ -113,6 +171,21 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==3.0.4" |             "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": { |         "coreapi": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb", |                 "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb", | ||||||
| @ -159,6 +232,13 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.5.1" |             "version": "==1.5.1" | ||||||
|         }, |         }, | ||||||
|  |         "daphne": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:1ca46d7419103958bbc9576fb7ba3b25b053006e22058bc97084ee1a7d44f4ba", | ||||||
|  |                 "sha256:aa64840015709bbc9daa3c4464a4a4d437937d6cda10a9b51e913eb319272553" | ||||||
|  |             ], | ||||||
|  |             "version": "==2.5.0" | ||||||
|  |         }, | ||||||
|         "defusedxml": { |         "defusedxml": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", |                 "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", | ||||||
| @ -265,6 +345,7 @@ | |||||||
|                 "sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32", |                 "sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32", | ||||||
|                 "sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b" |                 "sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|             "version": "==3.11.1" |             "version": "==3.11.1" | ||||||
|         }, |         }, | ||||||
|         "djangorestframework-guardian": { |         "djangorestframework-guardian": { | ||||||
| @ -281,6 +362,7 @@ | |||||||
|                 "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", |                 "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", | ||||||
|                 "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" |                 "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==0.15.2" |             "version": "==0.15.2" | ||||||
|         }, |         }, | ||||||
|         "drf-yasg": { |         "drf-yasg": { | ||||||
| @ -310,8 +392,109 @@ | |||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" |                 "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==0.18.2" |             "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": { |         "idna": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", |                 "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", | ||||||
| @ -319,11 +502,19 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==2.10" |             "version": "==2.10" | ||||||
|         }, |         }, | ||||||
|  |         "incremental": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", | ||||||
|  |                 "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3" | ||||||
|  |             ], | ||||||
|  |             "version": "==17.5.0" | ||||||
|  |         }, | ||||||
|         "inflection": { |         "inflection": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", |                 "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", | ||||||
|                 "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2" |                 "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|             "version": "==0.5.1" |             "version": "==0.5.1" | ||||||
|         }, |         }, | ||||||
|         "itypes": { |         "itypes": { | ||||||
| @ -338,6 +529,7 @@ | |||||||
|                 "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", |                 "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", | ||||||
|                 "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" |                 "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" |             "version": "==2.11.2" | ||||||
|         }, |         }, | ||||||
|         "jmespath": { |         "jmespath": { | ||||||
| @ -345,6 +537,7 @@ | |||||||
|                 "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", |                 "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", | ||||||
|                 "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" |                 "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==0.10.0" |             "version": "==0.10.0" | ||||||
|         }, |         }, | ||||||
|         "jsonschema": { |         "jsonschema": { | ||||||
| @ -359,11 +552,23 @@ | |||||||
|                 "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a", |                 "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a", | ||||||
|                 "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74" |                 "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" |             "version": "==4.6.11" | ||||||
|         }, |         }, | ||||||
|  |         "kubernetes": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:1a2472f8b01bc6aa87e3a34781f859bded5a5c8ff791a53d889a8bd6cc550430", | ||||||
|  |                 "sha256:4af81201520977139a143f96123fb789fa351879df37f122916b9b6ed050bbaf" | ||||||
|  |             ], | ||||||
|  |             "index": "pypi", | ||||||
|  |             "version": "==11.0.0" | ||||||
|  |         }, | ||||||
|         "ldap3": { |         "ldap3": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:59d1adcd5ead263387039e2a37d7cd772a2006b1cdb3ecfcbaab5192a601c515", |                 "sha256:59d1adcd5ead263387039e2a37d7cd772a2006b1cdb3ecfcbaab5192a601c515", | ||||||
|  |                 "sha256:7abbb3e5f4522114e0230ec175b60ae968b938d1f8a7d8bce7789f78d871fb9f", | ||||||
|  |                 "sha256:b399c39e80b6459e349b33fbe9787c1bcbf86de05994d41806a05c06f3e7574d", | ||||||
|  |                 "sha256:bdaf568cd30fc0006c8bb4f5e6014554afeb0c4bbea1677de9706e278a4057e7", | ||||||
|                 "sha256:df27407f4991f25bd669b5bb1bc8cb9ddf44a3e713ff6b3afeb3b3c26502f88f" |                 "sha256:df27407f4991f25bd669b5bb1bc8cb9ddf44a3e713ff6b3afeb3b3c26502f88f" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
| @ -442,13 +647,38 @@ | |||||||
|                 "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", |                 "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", | ||||||
|                 "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" |                 "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==1.1.1" |             "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": { |         "oauthlib": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", |                 "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", | ||||||
|                 "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" |                 "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==3.1.0" |             "version": "==3.1.0" | ||||||
|         }, |         }, | ||||||
|         "packaging": { |         "packaging": { | ||||||
| @ -504,15 +734,37 @@ | |||||||
|         }, |         }, | ||||||
|         "pyasn1": { |         "pyasn1": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|  |                 "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", | ||||||
|  |                 "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", | ||||||
|  |                 "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", | ||||||
|  |                 "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", | ||||||
|                 "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", |                 "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", | ||||||
|                 "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" |                 "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", | ||||||
|  |                 "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", | ||||||
|  |                 "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", | ||||||
|  |                 "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", | ||||||
|  |                 "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", | ||||||
|  |                 "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", | ||||||
|  |                 "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", | ||||||
|  |                 "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" | ||||||
|             ], |             ], | ||||||
|             "version": "==0.4.8" |             "version": "==0.4.8" | ||||||
|         }, |         }, | ||||||
|         "pyasn1-modules": { |         "pyasn1-modules": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|  |                 "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", | ||||||
|  |                 "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", | ||||||
|  |                 "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", | ||||||
|  |                 "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", | ||||||
|  |                 "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", | ||||||
|                 "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", |                 "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", | ||||||
|                 "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" |                 "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", | ||||||
|  |                 "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", | ||||||
|  |                 "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", | ||||||
|  |                 "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", | ||||||
|  |                 "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", | ||||||
|  |                 "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", | ||||||
|  |                 "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" | ||||||
|             ], |             ], | ||||||
|             "version": "==0.2.8" |             "version": "==0.2.8" | ||||||
|         }, |         }, | ||||||
| @ -521,6 +773,7 @@ | |||||||
|                 "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", |                 "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", | ||||||
|                 "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" |                 "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==2.20" |             "version": "==2.20" | ||||||
|         }, |         }, | ||||||
|         "pycryptodome": { |         "pycryptodome": { | ||||||
| @ -592,8 +845,17 @@ | |||||||
|                 "sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46", |                 "sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46", | ||||||
|                 "sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb" |                 "sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==3.9.8" |             "version": "==3.9.8" | ||||||
|         }, |         }, | ||||||
|  |         "pyhamcrest": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", | ||||||
|  |                 "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|  |             "version": "==2.0.2" | ||||||
|  |         }, | ||||||
|         "pyjwkest": { |         "pyjwkest": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:5560fd5ba08655f29ff6ad1df1e15dc05abc9d976fcbcec8d2b5167f49b70222" |                 "sha256:5560fd5ba08655f29ff6ad1df1e15dc05abc9d976fcbcec8d2b5167f49b70222" | ||||||
| @ -613,6 +875,7 @@ | |||||||
|                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", |                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", | ||||||
|                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" |                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==2.4.7" |             "version": "==2.4.7" | ||||||
|         }, |         }, | ||||||
|         "pyrsistent": { |         "pyrsistent": { | ||||||
| @ -626,6 +889,7 @@ | |||||||
|                 "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", |                 "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", | ||||||
|                 "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" |                 "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==2.8.1" |             "version": "==2.8.1" | ||||||
|         }, |         }, | ||||||
|         "pytz": { |         "pytz": { | ||||||
| @ -635,24 +899,6 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==2020.1" |             "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": { |         "pyyaml": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", |                 "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", | ||||||
| @ -683,6 +929,7 @@ | |||||||
|                 "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", |                 "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", | ||||||
|                 "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" |                 "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" |             "version": "==3.5.3" | ||||||
|         }, |         }, | ||||||
|         "requests": { |         "requests": { | ||||||
| @ -690,16 +937,26 @@ | |||||||
|                 "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", |                 "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", | ||||||
|                 "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" |                 "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" |             "version": "==2.24.0" | ||||||
|         }, |         }, | ||||||
|         "requests-oauthlib": { |         "requests-oauthlib": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", |                 "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", | ||||||
|                 "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" |                 "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", | ||||||
|  |                 "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.3.0" |             "version": "==1.3.0" | ||||||
|         }, |         }, | ||||||
|  |         "rsa": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa", | ||||||
|  |                 "sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|  |             "version": "==4.6" | ||||||
|  |         }, | ||||||
|         "ruamel.yaml": { |         "ruamel.yaml": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b", |                 "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b", | ||||||
| @ -729,7 +986,7 @@ | |||||||
|                 "sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad", |                 "sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad", | ||||||
|                 "sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e" |                 "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" |             "version": "==0.2.0" | ||||||
|         }, |         }, | ||||||
|         "s3transfer": { |         "s3transfer": { | ||||||
| @ -741,11 +998,11 @@ | |||||||
|         }, |         }, | ||||||
|         "sentry-sdk": { |         "sentry-sdk": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:5b884a391da04696c1d81d636d2ad728fd838370db1acdfda3acbad1fe5be830", |                 "sha256:0af429c221670e602f960fca85ca3f607c85510a91f11e8be8f742a978127f78", | ||||||
|                 "sha256:bbfe5633aee4dacb53d79d303ab6bfacf1749fb717750c112fb1658e5accce0d" |                 "sha256:a088a1054673c6a19ea590045c871c38da029ef743b61a07bfee95e9f3c060f7" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==0.17.2" |             "version": "==0.17.3" | ||||||
|         }, |         }, | ||||||
|         "service-identity": { |         "service-identity": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -768,6 +1025,7 @@ | |||||||
|                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", |                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", | ||||||
|                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" |                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==1.15.0" |             "version": "==1.15.0" | ||||||
|         }, |         }, | ||||||
|         "sqlparse": { |         "sqlparse": { | ||||||
| @ -775,6 +1033,7 @@ | |||||||
|                 "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", |                 "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", | ||||||
|                 "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" |                 "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==0.3.1" |             "version": "==0.3.1" | ||||||
|         }, |         }, | ||||||
|         "structlog": { |         "structlog": { | ||||||
| @ -793,11 +1052,52 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==2.7.3" |             "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": { |         "uritemplate": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", |                 "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", | ||||||
|                 "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" |                 "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==3.0.1" |             "version": "==3.0.1" | ||||||
|         }, |         }, | ||||||
|         "urllib3": { |         "urllib3": { | ||||||
| @ -809,15 +1109,119 @@ | |||||||
|                 "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" |                 "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "markers": null, |  | ||||||
|             "version": "==1.25.10" |             "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": { |         "vine": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87", |                 "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87", | ||||||
|                 "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af" |                 "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==1.3.0" |             "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": { |     "develop": { | ||||||
| @ -833,6 +1237,7 @@ | |||||||
|                 "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", |                 "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", | ||||||
|                 "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" |                 "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|             "version": "==3.2.10" |             "version": "==3.2.10" | ||||||
|         }, |         }, | ||||||
|         "astroid": { |         "astroid": { | ||||||
| @ -840,6 +1245,7 @@ | |||||||
|                 "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", |                 "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", | ||||||
|                 "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" |                 "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|             "version": "==2.4.1" |             "version": "==2.4.1" | ||||||
|         }, |         }, | ||||||
|         "attrs": { |         "attrs": { | ||||||
| @ -847,6 +1253,7 @@ | |||||||
|                 "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", |                 "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", | ||||||
|                 "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" |                 "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==20.1.0" |             "version": "==20.1.0" | ||||||
|         }, |         }, | ||||||
|         "autopep8": { |         "autopep8": { | ||||||
| @ -877,6 +1284,7 @@ | |||||||
|                 "sha256:477f0e18a0d58e50bb3dbc9af7fcda464fd0ebfc7a6151d8888602d7153171a0", |                 "sha256:477f0e18a0d58e50bb3dbc9af7fcda464fd0ebfc7a6151d8888602d7153171a0", | ||||||
|                 "sha256:cd4f3a231305e405ed8944d8ff35bd742d9bc740ad62f483bd0ca21ce7131984" |                 "sha256:cd4f3a231305e405ed8944d8ff35bd742d9bc740ad62f483bd0ca21ce7131984" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|             "version": "==1.0.0" |             "version": "==1.0.0" | ||||||
|         }, |         }, | ||||||
|         "bumpversion": { |         "bumpversion": { | ||||||
| @ -906,6 +1314,7 @@ | |||||||
|                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", |                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", | ||||||
|                 "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" |                 "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" |             "version": "==7.1.2" | ||||||
|         }, |         }, | ||||||
|         "colorama": { |         "colorama": { | ||||||
| @ -992,6 +1401,7 @@ | |||||||
|                 "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", |                 "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", | ||||||
|                 "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" |                 "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==3.8.3" |             "version": "==3.8.3" | ||||||
|         }, |         }, | ||||||
|         "flake8-polyfill": { |         "flake8-polyfill": { | ||||||
| @ -1006,6 +1416,7 @@ | |||||||
|                 "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", |                 "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", | ||||||
|                 "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" |                 "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.4'", | ||||||
|             "version": "==4.0.5" |             "version": "==4.0.5" | ||||||
|         }, |         }, | ||||||
|         "gitpython": { |         "gitpython": { | ||||||
| @ -1013,6 +1424,7 @@ | |||||||
|                 "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858", |                 "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858", | ||||||
|                 "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5" |                 "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.4'", | ||||||
|             "version": "==3.1.7" |             "version": "==3.1.7" | ||||||
|         }, |         }, | ||||||
|         "idna": { |         "idna": { | ||||||
| @ -1027,6 +1439,7 @@ | |||||||
|                 "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", |                 "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", | ||||||
|                 "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" |                 "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==4.3.21" |             "version": "==4.3.21" | ||||||
|         }, |         }, | ||||||
|         "lazy-object-proxy": { |         "lazy-object-proxy": { | ||||||
| @ -1053,6 +1466,7 @@ | |||||||
|                 "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", |                 "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", | ||||||
|                 "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" |                 "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==1.4.3" |             "version": "==1.4.3" | ||||||
|         }, |         }, | ||||||
|         "mccabe": { |         "mccabe": { | ||||||
| @ -1074,6 +1488,7 @@ | |||||||
|                 "sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea", |                 "sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea", | ||||||
|                 "sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15" |                 "sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.6'", | ||||||
|             "version": "==5.5.0" |             "version": "==5.5.0" | ||||||
|         }, |         }, | ||||||
|         "pep8-naming": { |         "pep8-naming": { | ||||||
| @ -1095,6 +1510,7 @@ | |||||||
|                 "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", |                 "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", | ||||||
|                 "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" |                 "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==2.6.0" |             "version": "==2.6.0" | ||||||
|         }, |         }, | ||||||
|         "pydocstyle": { |         "pydocstyle": { | ||||||
| @ -1102,6 +1518,7 @@ | |||||||
|                 "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", |                 "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", | ||||||
|                 "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678" |                 "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|             "version": "==5.1.1" |             "version": "==5.1.1" | ||||||
|         }, |         }, | ||||||
|         "pyflakes": { |         "pyflakes": { | ||||||
| @ -1109,6 +1526,7 @@ | |||||||
|                 "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", |                 "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", | ||||||
|                 "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" |                 "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==2.2.0" |             "version": "==2.2.0" | ||||||
|         }, |         }, | ||||||
|         "pylint": { |         "pylint": { | ||||||
| @ -1201,6 +1619,7 @@ | |||||||
|                 "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", |                 "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", | ||||||
|                 "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" |                 "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" |             "version": "==2.24.0" | ||||||
|         }, |         }, | ||||||
|         "requirements-detector": { |         "requirements-detector": { | ||||||
| @ -1228,6 +1647,7 @@ | |||||||
|                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", |                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", | ||||||
|                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" |                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==1.15.0" |             "version": "==1.15.0" | ||||||
|         }, |         }, | ||||||
|         "smmap": { |         "smmap": { | ||||||
| @ -1235,6 +1655,7 @@ | |||||||
|                 "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", |                 "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", | ||||||
|                 "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" |                 "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==3.0.4" |             "version": "==3.0.4" | ||||||
|         }, |         }, | ||||||
|         "snowballstemmer": { |         "snowballstemmer": { | ||||||
| @ -1249,6 +1670,7 @@ | |||||||
|                 "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", |                 "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", | ||||||
|                 "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" |                 "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==0.3.1" |             "version": "==0.3.1" | ||||||
|         }, |         }, | ||||||
|         "stevedore": { |         "stevedore": { | ||||||
| @ -1256,6 +1678,7 @@ | |||||||
|                 "sha256:a34086819e2c7a7f86d5635363632829dab8014e5fd7be2454c7cba84ac7514e", |                 "sha256:a34086819e2c7a7f86d5635363632829dab8014e5fd7be2454c7cba84ac7514e", | ||||||
|                 "sha256:ddc09a744dc224c84ec8e8efcb70595042d21c97c76df60daee64c9ad53bc7ee" |                 "sha256:ddc09a744dc224c84ec8e8efcb70595042d21c97c76df60daee64c9ad53bc7ee" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==3.2.1" |             "version": "==3.2.1" | ||||||
|         }, |         }, | ||||||
|         "toml": { |         "toml": { | ||||||
| @ -1308,7 +1731,6 @@ | |||||||
|                 "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" |                 "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "markers": null, |  | ||||||
|             "version": "==1.25.10" |             "version": "==1.25.10" | ||||||
|         }, |         }, | ||||||
|         "websocket-client": { |         "websocket-client": { | ||||||
|  | |||||||
| @ -4,7 +4,6 @@ | |||||||
|  |  | ||||||
| [](https://codecov.io/gh/BeryJu/passbook) | [](https://codecov.io/gh/BeryJu/passbook) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -261,18 +261,6 @@ stages: | |||||||
|             command: 'buildAndPush' |             command: 'buildAndPush' | ||||||
|             Dockerfile: 'Dockerfile' |             Dockerfile: 'Dockerfile' | ||||||
|             tags: 'gh-$(Build.SourceBranchName)' |             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 |       - job: build_static | ||||||
|         pool: |         pool: | ||||||
|           vmImage: 'ubuntu-latest' |           vmImage: 'ubuntu-latest' | ||||||
|  | |||||||
| @ -22,14 +22,13 @@ services: | |||||||
|       - traefik.enable=false |       - traefik.enable=false | ||||||
|   server: |   server: | ||||||
|     image: beryju/passbook:${PASSBOOK_TAG:-latest} |     image: beryju/passbook:${PASSBOOK_TAG:-latest} | ||||||
|     command: |     command: server | ||||||
|       - uwsgi |  | ||||||
|       - uwsgi.ini |  | ||||||
|     environment: |     environment: | ||||||
|       - PASSBOOK_REDIS__HOST=redis |       PASSBOOK_REDIS__HOST: redis | ||||||
|       - PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false} |       PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false} | ||||||
|       - PASSBOOK_POSTGRESQL__HOST=postgresql |       PASSBOOK_POSTGRESQL__HOST: postgresql | ||||||
|       - PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword} |       PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword} | ||||||
|  |       PASSBOOK_LOG_LEVEL: debug | ||||||
|     ports: |     ports: | ||||||
|       - 8000 |       - 8000 | ||||||
|     networks: |     networks: | ||||||
| @ -40,23 +39,17 @@ services: | |||||||
|       - traefik.frontend.rule=PathPrefix:/ |       - traefik.frontend.rule=PathPrefix:/ | ||||||
|   worker: |   worker: | ||||||
|     image: beryju/passbook:${PASSBOOK_TAG:-latest} |     image: beryju/passbook:${PASSBOOK_TAG:-latest} | ||||||
|     command: |     command: worker | ||||||
|       - celery |  | ||||||
|       - worker |  | ||||||
|       - --autoscale=10,3 |  | ||||||
|       - -E |  | ||||||
|       - -B |  | ||||||
|       - -A=passbook.root.celery |  | ||||||
|       - -s=/tmp/celerybeat-schedule |  | ||||||
|     networks: |     networks: | ||||||
|       - internal |       - internal | ||||||
|     labels: |     labels: | ||||||
|       - traefik.enable=false |       - traefik.enable=false | ||||||
|     environment: |     environment: | ||||||
|       - PASSBOOK_REDIS__HOST=redis |       PASSBOOK_REDIS__HOST: redis | ||||||
|       - PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false} |       PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false} | ||||||
|       - PASSBOOK_POSTGRESQL__HOST=postgresql |       PASSBOOK_POSTGRESQL__HOST: postgresql | ||||||
|       - PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword} |       PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword} | ||||||
|  |       PASSBOOK_LOG_LEVEL: debug | ||||||
|   static: |   static: | ||||||
|     image: beryju/passbook-static:latest |     image: beryju/passbook-static:latest | ||||||
|     networks: |     networks: | ||||||
|  | |||||||
| @ -1,3 +1,10 @@ | |||||||
| #!/bin/bash -ex | #!/bin/bash -e | ||||||
| /app/wait_for_db.py | /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: |     volumes: | ||||||
|       - /dev/shm:/dev/shm |       - /dev/shm:/dev/shm | ||||||
|     network_mode: host |     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 }} |         - name: {{ .Chart.Name }} | ||||||
|           image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" |           image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" | ||||||
|           imagePullPolicy: Always |           imagePullPolicy: Always | ||||||
|           args: |           args: server | ||||||
|             - uwsgi |  | ||||||
|             - uwsgi.ini |  | ||||||
|           envFrom: |           envFrom: | ||||||
|             - configMapRef: |             - configMapRef: | ||||||
|                 name: {{ include "passbook.fullname" . }}-config |                 name: {{ include "passbook.fullname" . }}-config | ||||||
|  | |||||||
| @ -26,14 +26,7 @@ spec: | |||||||
|         - name: {{ .Chart.Name }} |         - name: {{ .Chart.Name }} | ||||||
|           image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" |           image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" | ||||||
|           imagePullPolicy: IfNotPresent |           imagePullPolicy: IfNotPresent | ||||||
|           args: |           args: worker | ||||||
|             - celery |  | ||||||
|             - worker |  | ||||||
|             - --autoscale=10,3 |  | ||||||
|             - -E |  | ||||||
|             - -B |  | ||||||
|             - -A=passbook.root.celery |  | ||||||
|             - -s=/tmp/celerybeat-schedule |  | ||||||
|           envFrom: |           envFrom: | ||||||
|             - configMapRef: |             - configMapRef: | ||||||
|                 name: "{{ include "passbook.fullname" . }}-config" |                 name: "{{ include "passbook.fullname" . }}-config" | ||||||
|  | |||||||
| @ -46,6 +46,12 @@ | |||||||
|                         {% trans 'Providers' %} |                         {% trans 'Providers' %} | ||||||
|                     </a> |                     </a> | ||||||
|                 </li> |                 </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"> |                 <li class="pf-c-nav__item"> | ||||||
|                     <a href="{% url 'passbook_admin:property-mappings' %}" |                     <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' %}"> |                         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"> |                 <tr role="row"> | ||||||
|                     <th role="columnheader"> |                     <th role="columnheader"> | ||||||
|                         <div> |                         <div> | ||||||
|                             <div>{{ token.pk }}</div> |                             <div>{{ token.pk.hex }}</div> | ||||||
|                         </div> |                         </div> | ||||||
|                     </th> |                     </th> | ||||||
|                     <td role="cell"> |                     <td role="cell"> | ||||||
| @ -51,7 +51,11 @@ | |||||||
|                     </td> |                     </td> | ||||||
|                     <td role="cell"> |                     <td role="cell"> | ||||||
|                         <span> |                         <span> | ||||||
|  |                             {% if not token.expiring %} | ||||||
|  |                             - | ||||||
|  |                             {% else %} | ||||||
|                             {{ token.expires }} |                             {{ token.expires }} | ||||||
|  |                             {% endif %} | ||||||
|                         </span> |                         </span> | ||||||
|                     </td> |                     </td> | ||||||
|                     <td> |                     <td> | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ from passbook.admin.views import ( | |||||||
|     certificate_key_pair, |     certificate_key_pair, | ||||||
|     flows, |     flows, | ||||||
|     groups, |     groups, | ||||||
|  |     outposts, | ||||||
|     overview, |     overview, | ||||||
|     policies, |     policies, | ||||||
|     policies_bindings, |     policies_bindings, | ||||||
| @ -271,4 +272,19 @@ urlpatterns = [ | |||||||
|         certificate_key_pair.CertificateKeyPairDeleteView.as_view(), |         certificate_key_pair.CertificateKeyPairDeleteView.as_view(), | ||||||
|         name="certificatekeypair-delete", |         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 rest_framework import routers | ||||||
|  |  | ||||||
| from passbook.api.permissions import CustomObjectPermissions | from passbook.api.permissions import CustomObjectPermissions | ||||||
|  | from passbook.api.v2.messages import MessagesViewSet | ||||||
| from passbook.audit.api import EventViewSet | from passbook.audit.api import EventViewSet | ||||||
| from passbook.core.api.applications import ApplicationViewSet | from passbook.core.api.applications import ApplicationViewSet | ||||||
| from passbook.core.api.groups import GroupViewSet | 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.propertymappings import PropertyMappingViewSet | ||||||
| from passbook.core.api.providers import ProviderViewSet | from passbook.core.api.providers import ProviderViewSet | ||||||
| from passbook.core.api.sources import SourceViewSet | from passbook.core.api.sources import SourceViewSet | ||||||
| from passbook.core.api.users import UserViewSet | from passbook.core.api.users import UserViewSet | ||||||
|  | from passbook.crypto.api import CertificateKeyPairViewSet | ||||||
| from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet | from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet | ||||||
|  | from passbook.outposts.api import OutpostViewSet | ||||||
| from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet | from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet | ||||||
| from passbook.policies.dummy.api import DummyPolicyViewSet | from passbook.policies.dummy.api import DummyPolicyViewSet | ||||||
| from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet | 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.password.api import PasswordPolicyViewSet | ||||||
| from passbook.policies.reputation.api import ReputationPolicyViewSet | from passbook.policies.reputation.api import ReputationPolicyViewSet | ||||||
| from passbook.providers.oauth2.api import OAuth2ProviderViewSet, ScopeMappingViewSet | 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.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet | ||||||
| from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet | from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet | ||||||
| from passbook.sources.oauth.api import OAuthSourceViewSet | from passbook.sources.oauth.api import OAuthSourceViewSet | ||||||
| @ -47,10 +49,14 @@ from passbook.stages.user_write.api import UserWriteStageViewSet | |||||||
|  |  | ||||||
| router = routers.DefaultRouter() | router = routers.DefaultRouter() | ||||||
|  |  | ||||||
|  | router.register("root/messages", MessagesViewSet, basename="messages") | ||||||
| router.register("core/applications", ApplicationViewSet) | router.register("core/applications", ApplicationViewSet) | ||||||
| router.register("core/groups", GroupViewSet) | router.register("core/groups", GroupViewSet) | ||||||
| router.register("core/users", UserViewSet) | 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) | router.register("audit/events", EventViewSet) | ||||||
|  |  | ||||||
|  | |||||||
| @ -9,10 +9,11 @@ from passbook.lib.widgets import GroupedModelChoiceField | |||||||
| class ApplicationForm(forms.ModelForm): | class ApplicationForm(forms.ModelForm): | ||||||
|     """Application Form""" |     """Application Form""" | ||||||
|  |  | ||||||
|     provider = GroupedModelChoiceField( |     def __init__(self, *args, **kwargs): | ||||||
|         queryset=Provider.objects.all().order_by("pk").select_subclasses(), |         super().__init__(*args, **kwargs) | ||||||
|         required=False, |         self.fields["provider"].queryset = ( | ||||||
|     ) |             Provider.objects.all().order_by("pk").select_subclasses() | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
| @ -32,6 +33,7 @@ class ApplicationForm(forms.ModelForm): | |||||||
|             "meta_icon_url": forms.TextInput(), |             "meta_icon_url": forms.TextInput(), | ||||||
|             "meta_publisher": forms.TextInput(), |             "meta_publisher": forms.TextInput(), | ||||||
|         } |         } | ||||||
|  |         field_classes = {"provider": GroupedModelChoiceField} | ||||||
|         labels = { |         labels = { | ||||||
|             "meta_launch_url": _("Launch URL"), |             "meta_launch_url": _("Launch URL"), | ||||||
|             "meta_icon_url": _("Icon 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.")) |     name = models.TextField(help_text=_("User's display name.")) | ||||||
|  |  | ||||||
|     sources = models.ManyToManyField("Source", through="UserSourceConnection") |     sources = models.ManyToManyField("Source", through="UserSourceConnection") | ||||||
|     groups = models.ManyToManyField("Group") |     pb_groups = models.ManyToManyField("Group") | ||||||
|     password_change_date = models.DateTimeField(auto_now_add=True) |     password_change_date = models.DateTimeField(auto_now_add=True) | ||||||
|  |  | ||||||
|     attributes = models.JSONField(default=dict, blank=True) |     attributes = models.JSONField(default=dict, blank=True) | ||||||
|  | |||||||
| @ -8,6 +8,10 @@ | |||||||
| {% trans card_title %} | {% trans card_title %} | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block card_title %} | ||||||
|  | {% trans card_title %} | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
| {% block card %} | {% block card %} | ||||||
| <form method="POST" class="pf-c-form"> | <form method="POST" class="pf-c-form"> | ||||||
|     {% if message %} |     {% if message %} | ||||||
|  | |||||||
| @ -29,7 +29,7 @@ | |||||||
|         <main class="pf-c-login__main"> |         <main class="pf-c-login__main"> | ||||||
|             <header class="pf-c-login__main-header"> |             <header class="pf-c-login__main-header"> | ||||||
|                 <h1 class="pf-c-title pf-m-3xl"> |                 <h1 class="pf-c-title pf-m-3xl"> | ||||||
|                     {% block title %} |                     {% block card_title %} | ||||||
|                     {% endblock %} |                     {% endblock %} | ||||||
|                 </h1> |                 </h1> | ||||||
|             </header> |             </header> | ||||||
|  | |||||||
| @ -4,6 +4,10 @@ | |||||||
| {% load i18n %} | {% load i18n %} | ||||||
| {% load passbook_utils %} | {% load passbook_utils %} | ||||||
|  |  | ||||||
|  | {% block card_title %} | ||||||
|  | {% trans 'Permission denied' %} | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
| {% block title %} | {% block title %} | ||||||
| {% trans 'Permission denied' %} | {% trans 'Permission denied' %} | ||||||
| {% endblock %} | {% 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" |     verbose_name = "passbook Flows" | ||||||
|  |  | ||||||
|     def ready(self): |     def ready(self): | ||||||
|         """Flow signals that clear the cache""" |  | ||||||
|         import_module("passbook.flows.signals") |         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 collections.abc import Mapping | ||||||
| from contextlib import contextmanager | from contextlib import contextmanager | ||||||
| from glob import glob | from glob import glob | ||||||
|  | from json import dumps | ||||||
| from typing import Any, Dict | from typing import Any, Dict | ||||||
| from urllib.parse import urlparse | from urllib.parse import urlparse | ||||||
|  |  | ||||||
| import yaml | import yaml | ||||||
| from django.conf import ImproperlyConfigured | from django.conf import ImproperlyConfigured | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| from structlog import get_logger |  | ||||||
|  |  | ||||||
| SEARCH_PATHS = ["passbook/lib/default.yml", "/etc/passbook/config.yml", ""] + glob( | SEARCH_PATHS = ["passbook/lib/default.yml", "/etc/passbook/config.yml", ""] + glob( | ||||||
|     "/etc/passbook/config.d/*.yml", recursive=True |     "/etc/passbook/config.d/*.yml", recursive=True | ||||||
| ) | ) | ||||||
| LOGGER = get_logger() |  | ||||||
| ENV_PREFIX = "PASSBOOK" | ENV_PREFIX = "PASSBOOK" | ||||||
| ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local") | ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local") | ||||||
|  |  | ||||||
| @ -58,6 +57,13 @@ class ConfigLoader: | |||||||
|                         self.update_from_file(env_file) |                         self.update_from_file(env_file) | ||||||
|         self.update_from_env() |         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): |     def update(self, root, updatee): | ||||||
|         """Recursively update dictionary""" |         """Recursively update dictionary""" | ||||||
|         for key, value in updatee.items(): |         for key, value in updatee.items(): | ||||||
| @ -82,12 +88,14 @@ class ConfigLoader: | |||||||
|             with open(path) as file: |             with open(path) as file: | ||||||
|                 try: |                 try: | ||||||
|                     self.update(self.__config, yaml.safe_load(file)) |                     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) |                     self.loaded_file.append(path) | ||||||
|                 except yaml.YAMLError as exc: |                 except yaml.YAMLError as exc: | ||||||
|                     raise ImproperlyConfigured from exc |                     raise ImproperlyConfigured from exc | ||||||
|         except PermissionError as 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): |     def update_from_dict(self, update: dict): | ||||||
|         """Update config from dict""" |         """Update config from dict""" | ||||||
| @ -111,7 +119,7 @@ class ConfigLoader: | |||||||
|             current_obj[dot_parts[-1]] = value |             current_obj[dot_parts[-1]] = value | ||||||
|             idx += 1 |             idx += 1 | ||||||
|         if idx > 0: |         if idx > 0: | ||||||
|             LOGGER.debug("Loaded environment variables", count=idx) |             self._log("debug", "Loaded environment variables", count=idx) | ||||||
|             self.update(self.__config, outer) |             self.update(self.__config, outer) | ||||||
|  |  | ||||||
|     @contextmanager |     @contextmanager | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ redis: | |||||||
|   message_queue_db: 1 |   message_queue_db: 1 | ||||||
|  |  | ||||||
| debug: false | debug: false | ||||||
| log_level: warning | log_level: info | ||||||
|  |  | ||||||
| # Error reporting, sends stacktrace to sentry.beryju.org | # Error reporting, sends stacktrace to sentry.beryju.org | ||||||
| error_reporting: | 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> |                 <h1 class="pf-c-title pf-m-2xl">{% trans 'Setup with Kubernetes' %}</h1> | ||||||
|             </div> |             </div> | ||||||
|             <div class="pf-c-modal-box__body"> |             <div class="pf-c-modal-box__body"> | ||||||
|                 <p>{% trans 'Download the manifest to create the Gatekeeper deployment and service:' %}</p> |                 <p>{% trans 'Download the manifest to create the Proxy deployment and service:' %}</p> | ||||||
|                 <a href="{% url 'passbook_providers_app_gw:k8s-manifest' provider=provider.pk %}">{% trans 'Here' %}</a> |                 <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> |                 <p>{% trans 'Afterwards, add the following annotations to the Ingress you want to secure:' %}</p> | ||||||
|                 <textarea class="codemirror" readonly data-cm-mode="yaml"> |                 <textarea class="codemirror" readonly data-cm-mode="yaml"> | ||||||
| nginx.ingress.kubernetes.io/auth-signin: https://$host/oauth2/start?rd=$escaped_request_uri | 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": { |     "policies_reputation_ip_save": { | ||||||
|         "task": "passbook.policies.reputation.tasks.save_ip_reputation", |         "task": "passbook.policies.reputation.tasks.save_ip_reputation", | ||||||
|         "schedule": crontab(minute="*/5"), |         "schedule": crontab(minute="*/5"), | ||||||
|  |         "options": {"queue": "passbook_scheduled"}, | ||||||
|     }, |     }, | ||||||
|     "policies_reputation_user_save": { |     "policies_reputation_user_save": { | ||||||
|         "task": "passbook.policies.reputation.tasks.save_user_reputation", |         "task": "passbook.policies.reputation.tasks.save_user_reputation", | ||||||
|         "schedule": crontab(minute="*/5"), |         "schedule": crontab(minute="*/5"), | ||||||
|  |         "options": {"queue": "passbook_scheduled"}, | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
|  | |||||||
| @ -236,7 +236,7 @@ class OAuth2Provider(Provider): | |||||||
|         return OAuth2ProviderForm |         return OAuth2ProviderForm | ||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return self.name |         return f"OAuth2 Provider {self.name}" | ||||||
|  |  | ||||||
|     def html_setup_urls(self, request: HttpRequest) -> Optional[str]: |     def html_setup_urls(self, request: HttpRequest) -> Optional[str]: | ||||||
|         """return template and context modal with URLs for authorize, token, openid-config, etc""" |         """return template and context modal with URLs for authorize, token, openid-config, etc""" | ||||||
|  | |||||||
| @ -1,4 +1,6 @@ | |||||||
| """passbook OAuth2 OpenID well-known views""" | """passbook OAuth2 OpenID well-known views""" | ||||||
|  | from typing import Any, Dict | ||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse, JsonResponse | from django.http import HttpRequest, HttpResponse, JsonResponse | ||||||
| from django.shortcuts import get_object_or_404, reverse | from django.shortcuts import get_object_or_404, reverse | ||||||
| from django.views import View | from django.views import View | ||||||
| @ -16,6 +18,41 @@ PLAN_CONTEXT_SCOPES = "scopes" | |||||||
| class ProviderInfoView(View): | class ProviderInfoView(View): | ||||||
|     """OpenID-compliant Provider Info""" |     """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 |     # pylint: disable=unused-argument | ||||||
|     def get( |     def get( | ||||||
|         self, request: HttpRequest, application_slug: str, *args, **kwargs |         self, request: HttpRequest, application_slug: str, *args, **kwargs | ||||||
| @ -26,40 +63,7 @@ class ProviderInfoView(View): | |||||||
|         provider: OAuth2Provider = get_object_or_404( |         provider: OAuth2Provider = get_object_or_404( | ||||||
|             OAuth2Provider, pk=application.provider_id |             OAuth2Provider, pk=application.provider_id | ||||||
|         ) |         ) | ||||||
|         response = JsonResponse( |         response = JsonResponse(self.get_info(provider)) | ||||||
|             { |  | ||||||
|                 "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["Access-Control-Allow-Origin"] = "*" |         response["Access-Control-Allow-Origin"] = "*" | ||||||
|  |  | ||||||
|         return response |         return response | ||||||
|  | |||||||
| @ -1,10 +1,38 @@ | |||||||
| """ProxyProvider API Views""" | """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 rest_framework.viewsets import ModelViewSet | ||||||
|  |  | ||||||
|  | from passbook.providers.oauth2.views.provider import ProviderInfoView | ||||||
| from passbook.providers.proxy.models import ProxyProvider | 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): | class ProxyProviderSerializer(ModelSerializer): | ||||||
|     """ProxyProvider Serializer""" |     """ProxyProvider Serializer""" | ||||||
|  |  | ||||||
| @ -21,7 +49,13 @@ class ProxyProviderSerializer(ModelSerializer): | |||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         model = ProxyProvider |         model = ProxyProvider | ||||||
|         fields = ["pk", "name", "internal_host", "external_host"] |         fields = [ | ||||||
|  |             "pk", | ||||||
|  |             "name", | ||||||
|  |             "internal_host", | ||||||
|  |             "external_host", | ||||||
|  |             "certificate", | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class ProxyProviderViewSet(ModelViewSet): | class ProxyProviderViewSet(ModelViewSet): | ||||||
| @ -29,3 +63,47 @@ class ProxyProviderViewSet(ModelViewSet): | |||||||
|  |  | ||||||
|     queryset = ProxyProvider.objects.all() |     queryset = ProxyProvider.objects.all() | ||||||
|     serializer_class = ProxyProviderSerializer |     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" |     name = "passbook.providers.proxy" | ||||||
|     label = "passbook_providers_proxy" |     label = "passbook_providers_proxy" | ||||||
|     verbose_name = "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""" | """passbook Proxy Provider Forms""" | ||||||
| from django import 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 | from passbook.providers.proxy.models import ProxyProvider | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -9,14 +11,31 @@ class ProxyProviderForm(forms.ModelForm): | |||||||
|  |  | ||||||
|     instance: ProxyProvider |     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): |     def save(self, *args, **kwargs): | ||||||
|  |         actual_save = super().save(*args, **kwargs) | ||||||
|         self.instance.set_oauth_defaults() |         self.instance.set_oauth_defaults() | ||||||
|         return super().save(*args, **kwargs) |         self.instance.save() | ||||||
|  |         return actual_save | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         model = ProxyProvider |         model = ProxyProvider | ||||||
|         fields = ["name", "authorization_flow", "internal_host", "external_host"] |         fields = [ | ||||||
|  |             "name", | ||||||
|  |             "authorization_flow", | ||||||
|  |             "internal_host", | ||||||
|  |             "external_host", | ||||||
|  |             "certificate", | ||||||
|  |         ] | ||||||
|         widgets = { |         widgets = { | ||||||
|             "name": forms.TextInput(), |             "name": forms.TextInput(), | ||||||
|             "internal_host": 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""" | """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.core.validators import URLValidator | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.forms import ModelForm | from django.forms import ModelForm | ||||||
| from django.http import HttpRequest |  | ||||||
| from django.utils.translation import gettext as _ | 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 ( | from passbook.providers.oauth2.constants import ( | ||||||
|     SCOPE_OPENID, |     SCOPE_OPENID, | ||||||
|     SCOPE_OPENID_EMAIL, |     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 |     """Protect applications that don't support any of the other | ||||||
|     Protocols by using a Reverse-Proxy.""" |     Protocols by using a Reverse-Proxy.""" | ||||||
|  |  | ||||||
| @ -33,39 +47,42 @@ class ProxyProvider(OAuth2Provider): | |||||||
|         validators=[URLValidator(schemes=("http", "https"))] |         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]: |     def form(self) -> Type[ModelForm]: | ||||||
|         from passbook.providers.proxy.forms import ProxyProviderForm |         from passbook.providers.proxy.forms import ProxyProviderForm | ||||||
|  |  | ||||||
|         return 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): |     def set_oauth_defaults(self): | ||||||
|         """Ensure all OAuth2-related settings are correct""" |         """Ensure all OAuth2-related settings are correct""" | ||||||
|         self.client_type = ClientTypes.CONFIDENTIAL |         self.client_type = ClientTypes.CONFIDENTIAL | ||||||
|         self.response_type = ResponseTypes.CODE |         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( |         scopes = ScopeMapping.objects.filter( | ||||||
|             scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_PROFILE, SCOPE_OPENID_EMAIL] |             scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_PROFILE, SCOPE_OPENID_EMAIL] | ||||||
|         ) |         ) | ||||||
|         self.property_mappings.set(scopes) |         self.property_mappings.set(scopes) | ||||||
|         self.redirect_uris = "\n".join( |         self.redirect_uris = "\n".join( | ||||||
|             [ |             [ | ||||||
|                 f"{self.external_host}/oauth2/callback", |                 _get_callback_url(self.external_host), | ||||||
|                 f"{self.internal_host}/oauth2/callback", |                 _get_callback_url(self.internal_host), | ||||||
|             ] |             ] | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def __str__(self): |     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: |     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 |         return SAMLProviderForm | ||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return self.name |         return f"SAML Provider {self.name}" | ||||||
|  |  | ||||||
|     def link_download_metadata(self): |     def link_download_metadata(self): | ||||||
|         """Get link to download XML metadata for admin interface""" |         """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", |     "drf_yasg", | ||||||
|     "guardian", |     "guardian", | ||||||
|     "django_prometheus", |     "django_prometheus", | ||||||
|  |     "channels", | ||||||
|     "passbook.admin.apps.PassbookAdminConfig", |     "passbook.admin.apps.PassbookAdminConfig", | ||||||
|     "passbook.api.apps.PassbookAPIConfig", |     "passbook.api.apps.PassbookAPIConfig", | ||||||
|     "passbook.audit.apps.PassbookAuditConfig", |     "passbook.audit.apps.PassbookAuditConfig", | ||||||
|     "passbook.crypto.apps.PassbookCryptoConfig", |     "passbook.crypto.apps.PassbookCryptoConfig", | ||||||
|     "passbook.flows.apps.PassbookFlowsConfig", |     "passbook.flows.apps.PassbookFlowsConfig", | ||||||
|  |     "passbook.outposts.apps.PassbookOutpostConfig", | ||||||
|     "passbook.lib.apps.PassbookLibConfig", |     "passbook.lib.apps.PassbookLibConfig", | ||||||
|     "passbook.policies.apps.PassbookPoliciesConfig", |     "passbook.policies.apps.PassbookPoliciesConfig", | ||||||
|     "passbook.policies.dummy.apps.PassbookPolicyDummyConfig", |     "passbook.policies.dummy.apps.PassbookPolicyDummyConfig", | ||||||
| @ -125,13 +127,13 @@ REST_FRAMEWORK = { | |||||||
|     "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", |     "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", | ||||||
|     "PAGE_SIZE": 100, |     "PAGE_SIZE": 100, | ||||||
|     "DEFAULT_FILTER_BACKENDS": [ |     "DEFAULT_FILTER_BACKENDS": [ | ||||||
|  |         "rest_framework_guardian.filters.ObjectPermissionsFilter", | ||||||
|         "django_filters.rest_framework.DjangoFilterBackend", |         "django_filters.rest_framework.DjangoFilterBackend", | ||||||
|         "rest_framework.filters.OrderingFilter", |         "rest_framework.filters.OrderingFilter", | ||||||
|         "rest_framework.filters.SearchFilter", |         "rest_framework.filters.SearchFilter", | ||||||
|     ], |     ], | ||||||
|     "DEFAULT_PERMISSION_CLASSES": ( |     "DEFAULT_PERMISSION_CLASSES": ( | ||||||
|         "rest_framework.permissions.DjangoObjectPermissions", |         "rest_framework.permissions.DjangoObjectPermissions", | ||||||
|         "passbook.api.permissions.CustomObjectPermissions", |  | ||||||
|     ), |     ), | ||||||
|     "DEFAULT_AUTHENTICATION_CLASSES": ( |     "DEFAULT_AUTHENTICATION_CLASSES": ( | ||||||
|         "passbook.api.auth.PassbookTokenAuthentication", |         "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 | # Database | ||||||
| # https://docs.djangoproject.com/en/2.1/ref/settings/#databases | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases | ||||||
| @ -234,6 +244,7 @@ CELERY_BEAT_SCHEDULE = { | |||||||
|     "clean_expired_models": { |     "clean_expired_models": { | ||||||
|         "task": "passbook.core.tasks.clean_expired_models", |         "task": "passbook.core.tasks.clean_expired_models", | ||||||
|         "schedule": crontab(minute="*/5"),  # Run every 5 minutes |         "schedule": crontab(minute="*/5"),  # Run every 5 minutes | ||||||
|  |         "options": {"queue": "passbook_scheduled"}, | ||||||
|     } |     } | ||||||
| } | } | ||||||
| CELERY_CREATE_MISSING_QUEUES = True | CELERY_CREATE_MISSING_QUEUES = True | ||||||
| @ -364,6 +375,7 @@ _LOGGING_HANDLER_MAP = { | |||||||
|     "grpc": LOG_LEVEL, |     "grpc": LOG_LEVEL, | ||||||
|     "docker": "WARNING", |     "docker": "WARNING", | ||||||
|     "urllib3": "WARNING", |     "urllib3": "WARNING", | ||||||
|  |     "websockets": "WARNING", | ||||||
| } | } | ||||||
| for handler_name, level in _LOGGING_HANDLER_MAP.items(): | for handler_name, level in _LOGGING_HANDLER_MAP.items(): | ||||||
|     # pyright: reportGeneralTypeIssues=false |     # 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": { |     "sources_ldap_sync": { | ||||||
|         "task": "passbook.sources.ldap.tasks.sync", |         "task": "passbook.sources.ldap.tasks.sync", | ||||||
|         "schedule": crontab(minute=0),  # Run every hour |         "schedule": crontab(minute=0),  # Run every hour | ||||||
|  |         "options": {"queue": "passbook_scheduled"}, | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -5,5 +5,6 @@ CELERY_BEAT_SCHEDULE = { | |||||||
|     "saml_source_cleanup": { |     "saml_source_cleanup": { | ||||||
|         "task": "passbook.sources.saml.tasks.clean_temporary_users", |         "task": "passbook.sources.saml.tasks.clean_temporary_users", | ||||||
|         "schedule": crontab(minute="*/5"), |         "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 | 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