diff --git a/.coveragerc b/.coveragerc index 807014c55..a0001b5dc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,7 @@ [run] source = passbook omit = - */wsgi.py + */asgi.py manage.py */migrations/* */apps.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 020f8bf82..6517d0469 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: run: docker push beryju/passbook:0.9.0-stable - name: Push Docker Container to Registry (latest) run: docker push beryju/passbook:latest - build-gatekeeper: + build-proxy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 @@ -34,16 +34,16 @@ jobs: run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD - name: Building Docker Image run: | - cd gatekeeper + cd proxy docker build \ --no-cache \ - -t beryju/passbook-gatekeeper:0.9.0-stable \ - -t beryju/passbook-gatekeeper:latest \ + -t beryju/passbook-proxy:0.9.0-stable \ + -t beryju/passbook-proxy:latest \ -f Dockerfile . - name: Push Docker Container to Registry (versioned) - run: docker push beryju/passbook-gatekeeper:0.9.0-stable + run: docker push beryju/passbook-proxy:0.9.0-stable - name: Push Docker Container to Registry (latest) - run: docker push beryju/passbook-gatekeeper:latest + run: docker push beryju/passbook-proxy:latest build-static: runs-on: ubuntu-latest services: diff --git a/Dockerfile b/Dockerfile index e39439f9a..f8a5d951f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,14 +17,16 @@ COPY --from=locker /app/requirements-dev.txt /app/ WORKDIR /app/ RUN apt-get update && \ - apt-get install -y --no-install-recommends postgresql-client-11 && \ + apt-get install -y --no-install-recommends postgresql-client-11 build-essential && \ rm -rf /var/lib/apt/ && \ pip install -r requirements.txt --no-cache-dir && \ + apt-get remove --purge -y build-essential && \ + apt-get autoremove --purge && \ adduser --system --no-create-home --uid 1000 --group --home /app passbook COPY ./passbook/ /app/passbook COPY ./manage.py /app/ -COPY ./docker/uwsgi.ini /app/ +COPY ./docker/gunicorn.conf.py /app/ COPY ./docker/bootstrap.sh /bootstrap.sh COPY ./docker/wait_for_db.py /app/wait_for_db.py diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..ffb6150f2 --- /dev/null +++ b/Makefile @@ -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 diff --git a/Pipfile b/Pipfile index be3fe4fdd..e76bfb0cb 100644 --- a/Pipfile +++ b/Pipfile @@ -28,7 +28,8 @@ packaging = "*" psycopg2-binary = "*" pycryptodome = "*" pyjwkest = "*" -pyuwsgi = "*" +uvicorn = "*" +gunicorn = "*" pyyaml = "*" qrcode = "*" requests-oauthlib = "*" @@ -39,6 +40,9 @@ structlog = "*" swagger-spec-validator = "*" urllib3 = {extras = ["secure"],version = "*"} dacite = "*" +channels = "*" +channels-redis = "*" +kubernetes = "*" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 9ecc2feac..4b52112a9 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8f099b73d5993a0693261bf3d2b0e696d4f4d7ddd69a10d3db8ffe59a8ebd805" + "sha256": "a798bbd0b97857cac136c1743b8d6ad8bf8c3d95e2760c71d324bb2a7f47f678" }, "pipfile-spec": 6, "requires": { @@ -16,11 +16,19 @@ ] }, "default": { + "aioredis": { + "hashes": [ + "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a", + "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3" + ], + "version": "==1.3.1" + }, "amqp": { "hashes": [ "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21", "sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.6.1" }, "asgiref": { @@ -28,15 +36,40 @@ "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" ], + "markers": "python_version >= '3.5'", "version": "==3.2.10" }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "markers": "python_full_version >= '3.5.3'", + "version": "==3.0.1" + }, "attrs": { "hashes": [ "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.1.0" }, + "autobahn": { + "hashes": [ + "sha256:24ce276d313e84d68241c3aef30d484f352b90a40168981b3640312c821df77b", + "sha256:86bbce30cdd407137c57670993a8f9bfdfe3f8e994b889181d85e844d5aa8dfb" + ], + "markers": "python_version >= '3.5'", + "version": "==20.7.1" + }, + "automat": { + "hashes": [ + "sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33", + "sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111" + ], + "version": "==20.2.0" + }, "billiard": { "hashes": [ "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede", @@ -46,17 +79,26 @@ }, "boto3": { "hashes": [ - "sha256:b240ac281de363e25a8e1a4c862559d6a056d98dcb9f487fc94d73c6f6599dfc" + "sha256:4196b418598851ffd10cf9d1606694673cbfeca4ddf8b25d4e50addbd2fc60bf", + "sha256:69ad8f2184979e223e12ee3071674fdf910983cf9f4d6f34f7ec407b089064b5" ], "index": "pypi", - "version": "==1.14.53" + "version": "==1.14.54" }, "botocore": { "hashes": [ - "sha256:7e0272ceeb7747ed259a392e8d7b624cfd037085a8c59ef2b9f8916e7c556267", - "sha256:d37a83ac23257c85c48b74ab81173980234f8fc078e7a9d312d0ee7d057f90e6" + "sha256:6fe05837646447d61acdaf1e3401b92cd9309f00b19c577a50d0ade7735a3403", + "sha256:9e493a21e6a8d45c631eb2952ae8e1d0a31b9984546d4268ea10c0c33e2435ce" ], - "version": "==1.17.53" + "version": "==1.17.54" + }, + "cachetools": { + "hashes": [ + "sha256:513d4ff98dd27f85743a8dc0e92f55ddb1b49e060c2d5961512855cda2c01a98", + "sha256:bbaa39c3dede00175df2dc2b03d0cf18dd2d32a7de7beb68072d13043c9edb20" + ], + "markers": "python_version ~= '3.5'", + "version": "==4.1.1" }, "celery": { "hashes": [ @@ -106,6 +148,22 @@ ], "version": "==1.14.2" }, + "channels": { + "hashes": [ + "sha256:08e756406d7165cb32f6fc3090c0643f41ca9f7e0f7fada0b31194662f20f414", + "sha256:80a5ad1962ae039a3dcc0a5cb5212413e66e2f11ad9e9db8004834436daf3400" + ], + "index": "pypi", + "version": "==2.4.0" + }, + "channels-redis": { + "hashes": [ + "sha256:b4bcee949032cd838abdffd10da056930fca1a5a7ebc52139f8537aa622ac8d5", + "sha256:be7c14526ab924a091a66ad72a8be57a34900440b1126d520ac7742c0e2add03" + ], + "index": "pypi", + "version": "==3.0.1" + }, "chardet": { "hashes": [ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", @@ -113,6 +171,21 @@ ], "version": "==3.0.4" }, + "click": { + "hashes": [ + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==7.1.2" + }, + "constantly": { + "hashes": [ + "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", + "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d" + ], + "version": "==15.1.0" + }, "coreapi": { "hashes": [ "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb", @@ -159,6 +232,13 @@ "index": "pypi", "version": "==1.5.1" }, + "daphne": { + "hashes": [ + "sha256:1ca46d7419103958bbc9576fb7ba3b25b053006e22058bc97084ee1a7d44f4ba", + "sha256:aa64840015709bbc9daa3c4464a4a4d437937d6cda10a9b51e913eb319272553" + ], + "version": "==2.5.0" + }, "defusedxml": { "hashes": [ "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", @@ -265,6 +345,7 @@ "sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32", "sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b" ], + "markers": "python_version >= '3.5'", "version": "==3.11.1" }, "djangorestframework-guardian": { @@ -281,6 +362,7 @@ "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.15.2" }, "drf-yasg": { @@ -310,8 +392,109 @@ "hashes": [ "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.18.2" }, + "google-auth": { + "hashes": [ + "sha256:982e1f82cace752134660b4c0ff660761b32146a55abb3ad6d225529012af87c", + "sha256:f2498ad9cac3d2942d6c509ba18c4639656b366681881a1805f44f2a0c2d46f1" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.21.0" + }, + "gunicorn": { + "hashes": [ + "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", + "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" + ], + "index": "pypi", + "version": "==20.0.4" + }, + "h11": { + "hashes": [ + "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1", + "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1" + ], + "version": "==0.9.0" + }, + "hiredis": { + "hashes": [ + "sha256:06a039208f83744a702279b894c8cf24c14fd63c59cd917dcde168b79eef0680", + "sha256:0a909bf501459062aa1552be1461456518f367379fdc9fdb1f2ca5e4a1fdd7c0", + "sha256:18402d9e54fb278cb9a8c638df6f1550aca36a009d47ecf5aa263a38600f35b0", + "sha256:1e4cbbc3858ec7e680006e5ca590d89a5e083235988f26a004acf7244389ac01", + "sha256:23344e3c2177baf6975fbfa361ed92eb7d36d08f454636e5054b3faa7c2aff8a", + "sha256:289b31885b4996ce04cadfd5fc03d034dce8e2a8234479f7c9e23b9e245db06b", + "sha256:2c1c570ae7bf1bab304f29427e2475fe1856814312c4a1cf1cd0ee133f07a3c6", + "sha256:2c227c0ed371771ffda256034427320870e8ea2e4fd0c0a618c766e7c49aad73", + "sha256:3bb9b63d319402cead8bbd9dd55dca3b667d2997e9a0d8a1f9b6cc274db4baee", + "sha256:3ef2183de67b59930d2db8b8e8d4d58e00a50fcc5e92f4f678f6eed7a1c72d55", + "sha256:43b8ed3dbfd9171e44c554cb4acf4ee4505caa84c5e341858b50ea27dd2b6e12", + "sha256:47bcf3c5e6c1e87ceb86cdda2ee983fa0fe56a999e6185099b3c93a223f2fa9b", + "sha256:5263db1e2e1e8ae30500cdd75a979ff99dcc184201e6b4b820d0de74834d2323", + "sha256:5b1451727f02e7acbdf6aae4e06d75f66ee82966ff9114550381c3271a90f56c", + "sha256:6996883a8a6ff9117cbb3d6f5b0dcbbae6fb9e31e1a3e4e2f95e0214d9a1c655", + "sha256:6c96f64a54f030366657a54bb90b3093afc9c16c8e0dfa29fc0d6dbe169103a5", + "sha256:7332d5c3e35154cd234fd79573736ddcf7a0ade7a986db35b6196b9171493e75", + "sha256:7885b6f32c4a898e825bb7f56f36a02781ac4a951c63e4169f0afcf9c8c30dfb", + "sha256:7b0f63f10a166583ab744a58baad04e0f52cfea1ac27bfa1b0c21a48d1003c23", + "sha256:819f95d4eba3f9e484dd115ab7ab72845cf766b84286a00d4ecf76d33f1edca1", + "sha256:8968eeaa4d37a38f8ca1f9dbe53526b69628edc9c42229a5b2f56d98bb828c1f", + "sha256:89ebf69cb19a33d625db72d2ac589d26e936b8f7628531269accf4a3196e7872", + "sha256:8daecd778c1da45b8bd54fd41ffcd471a86beed3d8e57a43acf7a8d63bba4058", + "sha256:955ba8ea73cf3ed8bd2f963b4cb9f8f0dcb27becd2f4b3dd536fd24c45533454", + "sha256:964f18a59f5a64c0170f684c417f4fe3e695a536612e13074c4dd5d1c6d7c882", + "sha256:969843fbdfbf56cdb71da6f0bdf50f9985b8b8aeb630102945306cf10a9c6af2", + "sha256:996021ef33e0f50b97ff2d6b5f422a0fe5577de21a8873b58a779a5ddd1c3132", + "sha256:9e9c9078a7ce07e6fce366bd818be89365a35d2e4b163268f0ca9ba7e13bb2f6", + "sha256:a04901757cb0fb0f5602ac11dda48f5510f94372144d06c2563ba56c480b467c", + "sha256:a7bf1492429f18d205f3a818da3ff1f242f60aa59006e53dee00b4ef592a3363", + "sha256:aa0af2deb166a5e26e0d554b824605e660039b161e37ed4f01b8d04beec184f3", + "sha256:abfb15a6a7822f0fae681785cb38860e7a2cb1616a708d53df557b3d76c5bfd4", + "sha256:b253fe4df2afea4dfa6b1fa8c5fef212aff8bcaaeb4207e81eed05cb5e4a7919", + "sha256:b27f082f47d23cffc4cf1388b84fdc45c4ef6015f906cd7e0d988d9e35d36349", + "sha256:b33aea449e7f46738811fbc6f0b3177c6777a572207412bbbf6f525ffed001ae", + "sha256:b44f9421c4505c548435244d74037618f452844c5d3c67719d8a55e2613549da", + "sha256:bcc371151d1512201d0214c36c0c150b1dc64f19c2b1a8c9cb1d7c7c15ebd93f", + "sha256:c2851deeabd96d3f6283e9c6b26e0bfed4de2dc6fb15edf913e78b79fc5909ed", + "sha256:cdfd501c7ac5b198c15df800a3a34c38345f5182e5f80770caf362bccca65628", + "sha256:d2c0caffa47606d6d7c8af94ba42547bd2a441f06c74fd90a1ffe328524a6c64", + "sha256:dcb2db95e629962db5a355047fb8aefb012df6c8ae608930d391619dbd96fd86", + "sha256:e0eeb9c112fec2031927a1745788a181d0eecbacbed941fc5c4f7bc3f7b273bf", + "sha256:e154891263306200260d7f3051982774d7b9ef35af3509d5adbbe539afd2610c", + "sha256:e2e023a42dcbab8ed31f97c2bcdb980b7fbe0ada34037d87ba9d799664b58ded", + "sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390", + "sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.1.0" + }, + "httptools": { + "hashes": [ + "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be", + "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d", + "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce", + "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2", + "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6", + "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f", + "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009", + "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce", + "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a", + "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c", + "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4", + "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437" + ], + "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", + "version": "==0.1.1" + }, + "hyperlink": { + "hashes": [ + "sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af", + "sha256:c528d405766f15a2c536230de7e160b65a08e20264d8891b3eb03307b0df3c63" + ], + "version": "==20.0.1" + }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", @@ -319,11 +502,19 @@ ], "version": "==2.10" }, + "incremental": { + "hashes": [ + "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", + "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3" + ], + "version": "==17.5.0" + }, "inflection": { "hashes": [ "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2" ], + "markers": "python_version >= '3.5'", "version": "==0.5.1" }, "itypes": { @@ -338,6 +529,7 @@ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.11.2" }, "jmespath": { @@ -345,6 +537,7 @@ "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.0" }, "jsonschema": { @@ -359,11 +552,23 @@ "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a", "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==4.6.11" }, + "kubernetes": { + "hashes": [ + "sha256:1a2472f8b01bc6aa87e3a34781f859bded5a5c8ff791a53d889a8bd6cc550430", + "sha256:4af81201520977139a143f96123fb789fa351879df37f122916b9b6ed050bbaf" + ], + "index": "pypi", + "version": "==11.0.0" + }, "ldap3": { "hashes": [ "sha256:59d1adcd5ead263387039e2a37d7cd772a2006b1cdb3ecfcbaab5192a601c515", + "sha256:7abbb3e5f4522114e0230ec175b60ae968b938d1f8a7d8bce7789f78d871fb9f", + "sha256:b399c39e80b6459e349b33fbe9787c1bcbf86de05994d41806a05c06f3e7574d", + "sha256:bdaf568cd30fc0006c8bb4f5e6014554afeb0c4bbea1677de9706e278a4057e7", "sha256:df27407f4991f25bd669b5bb1bc8cb9ddf44a3e713ff6b3afeb3b3c26502f88f" ], "index": "pypi", @@ -442,13 +647,38 @@ "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, + "msgpack": { + "hashes": [ + "sha256:002a0d813e1f7b60da599bdf969e632074f9eec1b96cbed8fb0973a63160a408", + "sha256:25b3bc3190f3d9d965b818123b7752c5dfb953f0d774b454fd206c18fe384fb8", + "sha256:271b489499a43af001a2e42f42d876bb98ccaa7e20512ff37ca78c8e12e68f84", + "sha256:39c54fdebf5fa4dda733369012c59e7d085ebdfe35b6cf648f09d16708f1be5d", + "sha256:4233b7f86c1208190c78a525cd3828ca1623359ef48f78a6fea4b91bb995775a", + "sha256:5bea44181fc8e18eed1d0cd76e355073f00ce232ff9653a0ae88cb7d9e643322", + "sha256:5dba6d074fac9b24f29aaf1d2d032306c27f04187651511257e7831733293ec2", + "sha256:7a22c965588baeb07242cb561b63f309db27a07382825fc98aecaf0827c1538e", + "sha256:908944e3f038bca67fcfedb7845c4a257c7749bf9818632586b53bcf06ba4b97", + "sha256:9534d5cc480d4aff720233411a1f765be90885750b07df772380b34c10ecb5c0", + "sha256:aa5c057eab4f40ec47ea6f5a9825846be2ff6bf34102c560bad5cad5a677c5be", + "sha256:b3758dfd3423e358bbb18a7cccd1c74228dffa7a697e5be6cb9535de625c0dbf", + "sha256:c901e8058dd6653307906c5f157f26ed09eb94a850dddd989621098d347926ab", + "sha256:cec8bf10981ed70998d98431cd814db0ecf3384e6b113366e7f36af71a0fca08", + "sha256:db685187a415f51d6b937257474ca72199f393dad89534ebbdd7d7a3b000080e", + "sha256:e35b051077fc2f3ce12e7c6a34cf309680c63a842db3a0616ea6ed25ad20d272", + "sha256:e7bbdd8e2b277b77782f3ce34734b0dfde6cbe94ddb74de8d733d603c7f9e2b1", + "sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140" + ], + "version": "==1.0.0" + }, "oauthlib": { "hashes": [ "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.1.0" }, "packaging": { @@ -504,15 +734,37 @@ }, "pyasn1": { "hashes": [ + "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", + "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", + "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", + "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", - "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" + "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", + "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", + "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", + "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", + "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", + "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", + "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" ], "version": "==0.4.8" }, "pyasn1-modules": { "hashes": [ + "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", + "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", + "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", + "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", + "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", - "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" + "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", + "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", + "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", + "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", + "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", + "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", + "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" ], "version": "==0.2.8" }, @@ -521,6 +773,7 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pycryptodome": { @@ -592,8 +845,17 @@ "sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46", "sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.9.8" }, + "pyhamcrest": { + "hashes": [ + "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", + "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" + ], + "markers": "python_version >= '3.5'", + "version": "==2.0.2" + }, "pyjwkest": { "hashes": [ "sha256:5560fd5ba08655f29ff6ad1df1e15dc05abc9d976fcbcec8d2b5167f49b70222" @@ -613,6 +875,7 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "pyrsistent": { @@ -626,6 +889,7 @@ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.1" }, "pytz": { @@ -635,24 +899,6 @@ ], "version": "==2020.1" }, - "pyuwsgi": { - "hashes": [ - "sha256:1a4dd8d99b8497f109755e09484b0bd2aeaa533f7621e7c7e2a120a72111219d", - "sha256:206937deaebbac5c87692657c3151a5a9d40ecbc9b051b94154205c50a48e963", - "sha256:2cf35d9145208cc7c96464d688caa3de745bfc969e1a1ae23cb046fc10b0ac7e", - "sha256:3ab84a168633eeb55847d59475d86e9078d913d190c2a1aed804c562a10301a3", - "sha256:430406d1bcf288a87f14fde51c66877eaf5e98516838a1c6f761af5d814936fc", - "sha256:72be25ce7aa86c5616c59d12c2961b938e7bde47b7ff6a996ff83b89f7c5cd27", - "sha256:aa4d615de430e2066a1c76d9cc2a70abf2dfc703a82c21aee625b445866f2c3b", - "sha256:aadd231256a672cf4342ef9fb976051949e4d5b616195e696bcb7b8a9c07789e", - "sha256:b15ee6a7759b0465786d856334b8231d882deda5291cf243be6a343a8f3ef910", - "sha256:bd1d0a8d4cb87eb63417a72e6b1bac47053f9b0be550adc6d2a375f4cbaa22f0", - "sha256:d5787779ec24b67ac8898be9dc2b2b4e35f17d79f14361f6cf303d6283a848f2", - "sha256:ecfae85d6504e0ecbba100a795032a88ce8f110b62b93243f2df1bd116eca67f" - ], - "index": "pypi", - "version": "==2.0.19.1" - }, "pyyaml": { "hashes": [ "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", @@ -683,6 +929,7 @@ "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.5.3" }, "requests": { @@ -690,16 +937,26 @@ "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.24.0" }, "requests-oauthlib": { "hashes": [ "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", - "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" + "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", + "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" ], "index": "pypi", "version": "==1.3.0" }, + "rsa": { + "hashes": [ + "sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa", + "sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233" + ], + "markers": "python_version >= '3.5'", + "version": "==4.6" + }, "ruamel.yaml": { "hashes": [ "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b", @@ -729,7 +986,7 @@ "sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad", "sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e" ], - "markers": "platform_python_implementation == 'CPython' and python_version < '3.9'", + "markers": "python_version < '3.9' and platform_python_implementation == 'CPython'", "version": "==0.2.0" }, "s3transfer": { @@ -741,11 +998,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:5b884a391da04696c1d81d636d2ad728fd838370db1acdfda3acbad1fe5be830", - "sha256:bbfe5633aee4dacb53d79d303ab6bfacf1749fb717750c112fb1658e5accce0d" + "sha256:0af429c221670e602f960fca85ca3f607c85510a91f11e8be8f742a978127f78", + "sha256:a088a1054673c6a19ea590045c871c38da029ef743b61a07bfee95e9f3c060f7" ], "index": "pypi", - "version": "==0.17.2" + "version": "==0.17.3" }, "service-identity": { "hashes": [ @@ -768,6 +1025,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "sqlparse": { @@ -775,6 +1033,7 @@ "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.3.1" }, "structlog": { @@ -793,11 +1052,52 @@ "index": "pypi", "version": "==2.7.3" }, + "twisted": { + "extras": [ + "tls" + ], + "hashes": [ + "sha256:040eb6641125d2a9a09cf198ec7b83dd8858c6f51f6770325ed9959c00f5098f", + "sha256:147780b8caf21ba2aef3688628eaf13d7e7fe02a86747cd54bfaf2140538f042", + "sha256:158ddb80719a4813d292293ac44ba41d8b56555ed009d90994a278237ee63d2c", + "sha256:2182000d6ffc05d269e6c03bfcec8b57e20259ca1086180edaedec3f1e689292", + "sha256:25ffcf37944bdad4a99981bc74006d735a678d2b5c193781254fbbb6d69e3b22", + "sha256:3281d9ce889f7b21bdb73658e887141aa45a102baf3b2320eafcfba954fcefec", + "sha256:356e8d8dd3590e790e3dba4db139eb8a17aca64b46629c622e1b1597a4a92478", + "sha256:70952c56e4965b9f53b180daecf20a9595cf22b8d0935cd3bd664c90273c3ab2", + "sha256:7408c6635ee1b96587289283ebe90ee15dbf9614b05857b446055116bc822d29", + "sha256:7c547fd0215db9da8a1bc23182b309e84a232364cc26d829e9ee196ce840b114", + "sha256:894f6f3cfa57a15ea0d0714e4283913a5f2511dbd18653dd148eba53b3919797", + "sha256:94ac3d55a58c90e2075c5fe1853f2aa3892b73e3bf56395f743aefde8605eeaa", + "sha256:a58e61a2a01e5bcbe3b575c0099a2bcb8d70a75b1a087338e0c48dd6e01a5f15", + "sha256:c09c47ff9750a8e3aa60ad169c4b95006d455a29b80ad0901f031a103b2991cd", + "sha256:ca3a0b8c9110800e576d89b5337373e52018b41069bc879f12fa42b7eb2d0274", + "sha256:cd1dc5c85b58494138a3917752b54bb1daa0045d234b7c132c37a61d5483ebad", + "sha256:cdbc4c7f0cd7a2218b575844e970f05a1be1861c607b0e048c9bceca0c4d42f7", + "sha256:d267125cc0f1e8a0eed6319ba4ac7477da9b78a535601c49ecd20c875576433a", + "sha256:d72c55b5d56e176563b91d11952d13b01af8725c623e498db5507b6614fc1e10", + "sha256:d95803193561a243cb0401b0567c6b7987d3f2a67046770e1dccd1c9e49a9780", + "sha256:e92703bed0cc21d6cb5c61d66922b3b1564015ca8a51325bd164a5e33798d504", + "sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467", + "sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==20.3.0" + }, + "txaio": { + "hashes": [ + "sha256:17938f2bca4a9cabce61346758e482ca4e600160cbc28e861493eac74a19539d", + "sha256:38a469daf93c37e5527cb062653d6393ae11663147c42fab7ddc3f6d00d434ae" + ], + "markers": "python_version >= '3.5'", + "version": "==20.4.1" + }, "uritemplate": { "hashes": [ "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.1" }, "urllib3": { @@ -809,15 +1109,119 @@ "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], "index": "pypi", - "markers": null, "version": "==1.25.10" }, + "uvicorn": { + "hashes": [ + "sha256:46a83e371f37ea7ff29577d00015f02c942410288fb57def6440f2653fff1d26", + "sha256:4b70ddb4c1946e39db9f3082d53e323dfd50634b95fd83625d778729ef1730ef" + ], + "index": "pypi", + "version": "==0.11.8" + }, + "uvloop": { + "hashes": [ + "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", + "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", + "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", + "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", + "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", + "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", + "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", + "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", + "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" + ], + "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", + "version": "==0.14.0" + }, "vine": { "hashes": [ "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87", "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.3.0" + }, + "websocket-client": { + "hashes": [ + "sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549", + "sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010" + ], + "version": "==0.57.0" + }, + "websockets": { + "hashes": [ + "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", + "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", + "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", + "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", + "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", + "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", + "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", + "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", + "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", + "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", + "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", + "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", + "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", + "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", + "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", + "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", + "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", + "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", + "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", + "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", + "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", + "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==8.1" + }, + "zope.interface": { + "hashes": [ + "sha256:0103cba5ed09f27d2e3de7e48bb320338592e2fabc5ce1432cf33808eb2dfd8b", + "sha256:14415d6979356629f1c386c8c4249b4d0082f2ea7f75871ebad2e29584bd16c5", + "sha256:1ae4693ccee94c6e0c88a4568fb3b34af8871c60f5ba30cf9f94977ed0e53ddd", + "sha256:1b87ed2dc05cb835138f6a6e3595593fea3564d712cb2eb2de963a41fd35758c", + "sha256:269b27f60bcf45438e8683269f8ecd1235fa13e5411de93dae3b9ee4fe7f7bc7", + "sha256:27d287e61639d692563d9dab76bafe071fbeb26818dd6a32a0022f3f7ca884b5", + "sha256:39106649c3082972106f930766ae23d1464a73b7d30b3698c986f74bf1256a34", + "sha256:40e4c42bd27ed3c11b2c983fecfb03356fae1209de10686d03c02c8696a1d90e", + "sha256:461d4339b3b8f3335d7e2c90ce335eb275488c587b61aca4b305196dde2ff086", + "sha256:4f98f70328bc788c86a6a1a8a14b0ea979f81ae6015dd6c72978f1feff70ecda", + "sha256:558a20a0845d1a5dc6ff87cd0f63d7dac982d7c3be05d2ffb6322a87c17fa286", + "sha256:562dccd37acec149458c1791da459f130c6cf8902c94c93b8d47c6337b9fb826", + "sha256:5e86c66a6dea8ab6152e83b0facc856dc4d435fe0f872f01d66ce0a2131b7f1d", + "sha256:60a207efcd8c11d6bbeb7862e33418fba4e4ad79846d88d160d7231fcb42a5ee", + "sha256:645a7092b77fdbc3f68d3cc98f9d3e71510e419f54019d6e282328c0dd140dcd", + "sha256:6874367586c020705a44eecdad5d6b587c64b892e34305bb6ed87c9bbe22a5e9", + "sha256:74bf0a4f9091131de09286f9a605db449840e313753949fe07c8d0fe7659ad1e", + "sha256:7b726194f938791a6691c7592c8b9e805fc6d1b9632a833b9c0640828cd49cbc", + "sha256:8149ded7f90154fdc1a40e0c8975df58041a6f693b8f7edcd9348484e9dc17fe", + "sha256:8cccf7057c7d19064a9e27660f5aec4e5c4001ffcf653a47531bde19b5aa2a8a", + "sha256:911714b08b63d155f9c948da2b5534b223a1a4fc50bb67139ab68b277c938578", + "sha256:a5f8f85986197d1dd6444763c4a15c991bfed86d835a1f6f7d476f7198d5f56a", + "sha256:a744132d0abaa854d1aad50ba9bc64e79c6f835b3e92521db4235a1991176813", + "sha256:af2c14efc0bb0e91af63d00080ccc067866fb8cbbaca2b0438ab4105f5e0f08d", + "sha256:b054eb0a8aa712c8e9030065a59b5e6a5cf0746ecdb5f087cca5ec7685690c19", + "sha256:b0becb75418f8a130e9d465e718316cd17c7a8acce6fe8fe07adc72762bee425", + "sha256:b1d2ed1cbda2ae107283befd9284e650d840f8f7568cb9060b5466d25dc48975", + "sha256:ba4261c8ad00b49d48bbb3b5af388bb7576edfc0ca50a49c11dcb77caa1d897e", + "sha256:d1fe9d7d09bb07228650903d6a9dc48ea649e3b8c69b1d263419cc722b3938e8", + "sha256:d7804f6a71fc2dda888ef2de266727ec2f3915373d5a785ed4ddc603bbc91e08", + "sha256:da2844fba024dd58eaa712561da47dcd1e7ad544a257482392472eae1c86d5e5", + "sha256:dcefc97d1daf8d55199420e9162ab584ed0893a109f45e438b9794ced44c9fd0", + "sha256:dd98c436a1fc56f48c70882cc243df89ad036210d871c7427dc164b31500dc11", + "sha256:e74671e43ed4569fbd7989e5eecc7d06dc134b571872ab1d5a88f4a123814e9f", + "sha256:eb9b92f456ff3ec746cd4935b73c1117538d6124b8617bc0fe6fda0b3816e345", + "sha256:ebb4e637a1fb861c34e48a00d03cffa9234f42bef923aec44e5625ffb9a8e8f9", + "sha256:ef739fe89e7f43fb6494a43b1878a36273e5924869ba1d866f752c5812ae8d58", + "sha256:f40db0e02a8157d2b90857c24d89b6310f9b6c3642369852cdc3b5ac49b92afc", + "sha256:f68bf937f113b88c866d090fea0bc52a098695173fc613b055a17ff0cf9683b6", + "sha256:fb55c182a3f7b84c1a2d6de5fa7b1a05d4660d866b91dbf8d74549c57a1499e8" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==5.1.0" } }, "develop": { @@ -833,6 +1237,7 @@ "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" ], + "markers": "python_version >= '3.5'", "version": "==3.2.10" }, "astroid": { @@ -840,6 +1245,7 @@ "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" ], + "markers": "python_version >= '3.5'", "version": "==2.4.1" }, "attrs": { @@ -847,6 +1253,7 @@ "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.1.0" }, "autopep8": { @@ -877,6 +1284,7 @@ "sha256:477f0e18a0d58e50bb3dbc9af7fcda464fd0ebfc7a6151d8888602d7153171a0", "sha256:cd4f3a231305e405ed8944d8ff35bd742d9bc740ad62f483bd0ca21ce7131984" ], + "markers": "python_version >= '3.5'", "version": "==1.0.0" }, "bumpversion": { @@ -906,6 +1314,7 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "colorama": { @@ -992,6 +1401,7 @@ "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.8.3" }, "flake8-polyfill": { @@ -1006,6 +1416,7 @@ "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" ], + "markers": "python_version >= '3.4'", "version": "==4.0.5" }, "gitpython": { @@ -1013,6 +1424,7 @@ "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858", "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5" ], + "markers": "python_version >= '3.4'", "version": "==3.1.7" }, "idna": { @@ -1027,6 +1439,7 @@ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==4.3.21" }, "lazy-object-proxy": { @@ -1053,6 +1466,7 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -1074,6 +1488,7 @@ "sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea", "sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15" ], + "markers": "python_version >= '2.6'", "version": "==5.5.0" }, "pep8-naming": { @@ -1095,6 +1510,7 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pydocstyle": { @@ -1102,6 +1518,7 @@ "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678" ], + "markers": "python_version >= '3.5'", "version": "==5.1.1" }, "pyflakes": { @@ -1109,6 +1526,7 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pylint": { @@ -1201,6 +1619,7 @@ "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.24.0" }, "requirements-detector": { @@ -1228,6 +1647,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "smmap": { @@ -1235,6 +1655,7 @@ "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.4" }, "snowballstemmer": { @@ -1249,6 +1670,7 @@ "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.3.1" }, "stevedore": { @@ -1256,6 +1678,7 @@ "sha256:a34086819e2c7a7f86d5635363632829dab8014e5fd7be2454c7cba84ac7514e", "sha256:ddc09a744dc224c84ec8e8efcb70595042d21c97c76df60daee64c9ad53bc7ee" ], + "markers": "python_version >= '3.6'", "version": "==3.2.1" }, "toml": { @@ -1308,7 +1731,6 @@ "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], "index": "pypi", - "markers": null, "version": "==1.25.10" }, "websocket-client": { diff --git a/README.md b/README.md index 1b8cc91c7..9b9ff48e2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ ![Tests](https://img.shields.io/azure-devops/tests/beryjuorg/passbook/1?compact_message&style=flat-square) [![Code Coverage](https://img.shields.io/codecov/c/gh/beryju/passbook?style=flat-square)](https://codecov.io/gh/BeryJu/passbook) ![Docker pulls](https://img.shields.io/docker/pulls/beryju/passbook.svg?style=flat-square) -![Docker pulls (gatekeeper)](https://img.shields.io/docker/pulls/beryju/passbook-gatekeeper.svg?style=flat-square) ![Latest version](https://img.shields.io/docker/v/beryju/passbook?sort=semver&style=flat-square) ![LGTM Grade](https://img.shields.io/lgtm/grade/python/github/BeryJu/passbook?style=flat-square) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e6e9f5f8b..11f208f6f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -261,18 +261,6 @@ stages: command: 'buildAndPush' Dockerfile: 'Dockerfile' tags: 'gh-$(Build.SourceBranchName)' - - job: build_gatekeeper - pool: - vmImage: 'ubuntu-latest' - steps: - - task: Docker@2 - inputs: - containerRegistry: 'dockerhub' - repository: 'beryju/passbook-gatekeeper' - command: 'buildAndPush' - Dockerfile: 'gatekeeper/Dockerfile' - buildContext: 'gatekeeper/' - tags: 'gh-$(Build.SourceBranchName)' - job: build_static pool: vmImage: 'ubuntu-latest' diff --git a/docker-compose.yml b/docker-compose.yml index fe33b30bd..1d54892e3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,14 +22,13 @@ services: - traefik.enable=false server: image: beryju/passbook:${PASSBOOK_TAG:-latest} - command: - - uwsgi - - uwsgi.ini + command: server environment: - - PASSBOOK_REDIS__HOST=redis - - PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false} - - PASSBOOK_POSTGRESQL__HOST=postgresql - - PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword} + PASSBOOK_REDIS__HOST: redis + PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false} + PASSBOOK_POSTGRESQL__HOST: postgresql + PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword} + PASSBOOK_LOG_LEVEL: debug ports: - 8000 networks: @@ -40,23 +39,17 @@ services: - traefik.frontend.rule=PathPrefix:/ worker: image: beryju/passbook:${PASSBOOK_TAG:-latest} - command: - - celery - - worker - - --autoscale=10,3 - - -E - - -B - - -A=passbook.root.celery - - -s=/tmp/celerybeat-schedule + command: worker networks: - internal labels: - traefik.enable=false environment: - - PASSBOOK_REDIS__HOST=redis - - PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false} - - PASSBOOK_POSTGRESQL__HOST=postgresql - - PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword} + PASSBOOK_REDIS__HOST: redis + PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false} + PASSBOOK_POSTGRESQL__HOST: postgresql + PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword} + PASSBOOK_LOG_LEVEL: debug static: image: beryju/passbook-static:latest networks: diff --git a/docker/bootstrap.sh b/docker/bootstrap.sh index d99696622..8cac56846 100755 --- a/docker/bootstrap.sh +++ b/docker/bootstrap.sh @@ -1,3 +1,10 @@ -#!/bin/bash -ex +#!/bin/bash -e /app/wait_for_db.py -"$@" +printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@" +if [[ "$1" == "server" ]]; then + gunicorn -c gunicorn.conf.py passbook.root.asgi:application +elif [[ "$1" == "worker" ]]; then + celery worker --autoscale=10,3 -E -B -A=passbook.root.celery -s=/tmp/celerybeat-schedule +else + ./manage.py "$@" +fi diff --git a/docker/gunicorn.conf.py b/docker/gunicorn.conf.py new file mode 100644 index 000000000..742240854 --- /dev/null +++ b/docker/gunicorn.conf.py @@ -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"}, + }, +} diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini deleted file mode 100644 index fc0600844..000000000 --- a/docker/uwsgi.ini +++ /dev/null @@ -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 diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 367d8485a..d8ce2b8d3 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -6,3 +6,4 @@ services: volumes: - /dev/shm:/dev/shm network_mode: host + restart: always diff --git a/gatekeeper/Dockerfile b/gatekeeper/Dockerfile deleted file mode 100644 index 7d0b26ad6..000000000 --- a/gatekeeper/Dockerfile +++ /dev/null @@ -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 diff --git a/helm/templates/web-deployment.yaml b/helm/templates/web-deployment.yaml index 06be6d13b..c33c7acdf 100644 --- a/helm/templates/web-deployment.yaml +++ b/helm/templates/web-deployment.yaml @@ -53,9 +53,7 @@ spec: - name: {{ .Chart.Name }} image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" imagePullPolicy: Always - args: - - uwsgi - - uwsgi.ini + args: server envFrom: - configMapRef: name: {{ include "passbook.fullname" . }}-config diff --git a/helm/templates/worker-deployment.yaml b/helm/templates/worker-deployment.yaml index 6be095439..dca135e80 100644 --- a/helm/templates/worker-deployment.yaml +++ b/helm/templates/worker-deployment.yaml @@ -26,14 +26,7 @@ spec: - name: {{ .Chart.Name }} image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" imagePullPolicy: IfNotPresent - args: - - celery - - worker - - --autoscale=10,3 - - -E - - -B - - -A=passbook.root.celery - - -s=/tmp/celerybeat-schedule + args: worker envFrom: - configMapRef: name: "{{ include "passbook.fullname" . }}-config" diff --git a/passbook/admin/templates/administration/base.html b/passbook/admin/templates/administration/base.html index cfcd243f3..fd90d0512 100644 --- a/passbook/admin/templates/administration/base.html +++ b/passbook/admin/templates/administration/base.html @@ -46,6 +46,12 @@ {% trans 'Providers' %} +
  • + + {% trans 'Outposts' %} + +
  • diff --git a/passbook/admin/templates/administration/outpost/list.html b/passbook/admin/templates/administration/outpost/list.html new file mode 100644 index 000000000..38e97844c --- /dev/null +++ b/passbook/admin/templates/administration/outpost/list.html @@ -0,0 +1,96 @@ +{% extends "administration/base.html" %} + +{% load i18n %} +{% load humanize %} +{% load passbook_utils %} + +{% block head %} +{{ block.super }} + +{% endblock %} + +{% block content %} +
    +
    +

    + + {% trans 'Outposts' %} +

    +

    {% trans "Outposts are deployments of passbook components to support different environments and protocols, like reverse proxies." %}

    +
    +
    +
    +
    + {% if object_list %} +
    +
    + + {% include 'partials/pagination.html' %} +
    +
    + + + + + + + + + + + {% for outpost in object_list %} + + + + + + + {% endfor %} + +
    {% trans 'Name' %}{% trans 'Providers' %}{% trans 'Health' %}
    + {{ outpost.name }} + + + {{ outpost.providers.all.select_subclasses|join:", " }} + + + {% with health=outpost.health %} + {% if health %} + {{ health|naturaltime }} + {% else %} + Unhealthy + {% endif %} + {% endwith %} + + {% trans 'Edit' %} + {% trans 'Delete' %} +
    +
    + {% include 'partials/pagination.html' %} +
    + {% else %} +
    +
    + +

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

    +
    + {% trans 'Currently no outposts exist. Click the button below to create one.' %} +
    + {% trans 'Create' %} +
    +
    + {% endif %} +
    +
    +{% endblock %} diff --git a/passbook/admin/templates/administration/token/list.html b/passbook/admin/templates/administration/token/list.html index 2df38c0a8..c73489774 100644 --- a/passbook/admin/templates/administration/token/list.html +++ b/passbook/admin/templates/administration/token/list.html @@ -36,7 +36,7 @@
    -
    {{ token.pk }}
    +
    {{ token.pk.hex }}
    @@ -51,7 +51,11 @@ + {% if not token.expiring %} + - + {% else %} {{ token.expires }} + {% endif %} diff --git a/passbook/admin/urls.py b/passbook/admin/urls.py index 3c2771f5d..09d90d062 100644 --- a/passbook/admin/urls.py +++ b/passbook/admin/urls.py @@ -6,6 +6,7 @@ from passbook.admin.views import ( certificate_key_pair, flows, groups, + outposts, overview, policies, policies_bindings, @@ -271,4 +272,19 @@ urlpatterns = [ certificate_key_pair.CertificateKeyPairDeleteView.as_view(), name="certificatekeypair-delete", ), + # Outposts + path("outposts/", outposts.OutpostListView.as_view(), name="outposts",), + path( + "outposts/create/", outposts.OutpostCreateView.as_view(), name="outpost-create", + ), + path( + "outposts//update/", + outposts.OutpostUpdateView.as_view(), + name="outpost-update", + ), + path( + "outposts//delete/", + outposts.OutpostDeleteView.as_view(), + name="outpost-delete", + ), ] diff --git a/passbook/admin/views/outposts.py b/passbook/admin/views/outposts.py new file mode 100644 index 000000000..b3ecd555e --- /dev/null +++ b/passbook/admin/views/outposts.py @@ -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") diff --git a/passbook/core/api/messages.py b/passbook/api/v2/messages.py similarity index 100% rename from passbook/core/api/messages.py rename to passbook/api/v2/messages.py diff --git a/passbook/api/v2/urls.py b/passbook/api/v2/urls.py index 1fa630d22..2acadd0c3 100644 --- a/passbook/api/v2/urls.py +++ b/passbook/api/v2/urls.py @@ -6,15 +6,17 @@ from drf_yasg.views import get_schema_view from rest_framework import routers from passbook.api.permissions import CustomObjectPermissions +from passbook.api.v2.messages import MessagesViewSet from passbook.audit.api import EventViewSet from passbook.core.api.applications import ApplicationViewSet from passbook.core.api.groups import GroupViewSet -from passbook.core.api.messages import MessagesViewSet from passbook.core.api.propertymappings import PropertyMappingViewSet from passbook.core.api.providers import ProviderViewSet from passbook.core.api.sources import SourceViewSet from passbook.core.api.users import UserViewSet +from passbook.crypto.api import CertificateKeyPairViewSet from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet +from passbook.outposts.api import OutpostViewSet from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet from passbook.policies.dummy.api import DummyPolicyViewSet from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet @@ -24,7 +26,7 @@ from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet from passbook.policies.password.api import PasswordPolicyViewSet from passbook.policies.reputation.api import ReputationPolicyViewSet from passbook.providers.oauth2.api import OAuth2ProviderViewSet, ScopeMappingViewSet -from passbook.providers.proxy.api import ProxyProviderViewSet +from passbook.providers.proxy.api import OutpostConfigViewSet, ProxyProviderViewSet from passbook.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet from passbook.sources.oauth.api import OAuthSourceViewSet @@ -47,10 +49,14 @@ from passbook.stages.user_write.api import UserWriteStageViewSet router = routers.DefaultRouter() +router.register("root/messages", MessagesViewSet, basename="messages") router.register("core/applications", ApplicationViewSet) router.register("core/groups", GroupViewSet) router.register("core/users", UserViewSet) -router.register("core/messages", MessagesViewSet, basename="messages") +router.register("outposts/outposts", OutpostViewSet) +router.register("outposts/proxy", OutpostConfigViewSet) + +router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet) router.register("audit/events", EventViewSet) diff --git a/passbook/core/forms/applications.py b/passbook/core/forms/applications.py index bf796ad1f..c35a6bf7c 100644 --- a/passbook/core/forms/applications.py +++ b/passbook/core/forms/applications.py @@ -9,10 +9,11 @@ from passbook.lib.widgets import GroupedModelChoiceField class ApplicationForm(forms.ModelForm): """Application Form""" - provider = GroupedModelChoiceField( - queryset=Provider.objects.all().order_by("pk").select_subclasses(), - required=False, - ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["provider"].queryset = ( + Provider.objects.all().order_by("pk").select_subclasses() + ) class Meta: @@ -32,6 +33,7 @@ class ApplicationForm(forms.ModelForm): "meta_icon_url": forms.TextInput(), "meta_publisher": forms.TextInput(), } + field_classes = {"provider": GroupedModelChoiceField} labels = { "meta_launch_url": _("Launch URL"), "meta_icon_url": _("Icon URL"), diff --git a/passbook/core/migrations/0008_auto_20200824_1532.py b/passbook/core/migrations/0008_auto_20200824_1532.py new file mode 100644 index 000000000..23ac85430 --- /dev/null +++ b/passbook/core/migrations/0008_auto_20200824_1532.py @@ -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"), + ), + ] diff --git a/passbook/core/models.py b/passbook/core/models.py index 756bbe1b1..db93f8b10 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -58,7 +58,7 @@ class User(GuardianUserMixin, AbstractUser): name = models.TextField(help_text=_("User's display name.")) sources = models.ManyToManyField("Source", through="UserSourceConnection") - groups = models.ManyToManyField("Group") + pb_groups = models.ManyToManyField("Group") password_change_date = models.DateTimeField(auto_now_add=True) attributes = models.JSONField(default=dict, blank=True) diff --git a/passbook/core/templates/error/generic.html b/passbook/core/templates/error/generic.html index 30b7e9386..8dd8d13da 100644 --- a/passbook/core/templates/error/generic.html +++ b/passbook/core/templates/error/generic.html @@ -8,6 +8,10 @@ {% trans card_title %} {% endblock %} +{% block card_title %} +{% trans card_title %} +{% endblock %} + {% block card %}
    {% if message %} diff --git a/passbook/core/templates/login/base_full.html b/passbook/core/templates/login/base_full.html index 127e389ca..2fe84cd24 100644 --- a/passbook/core/templates/login/base_full.html +++ b/passbook/core/templates/login/base_full.html @@ -29,7 +29,7 @@
    diff --git a/passbook/core/templates/login/denied.html b/passbook/core/templates/login/denied.html index 4d8ac623a..cbf4d8768 100644 --- a/passbook/core/templates/login/denied.html +++ b/passbook/core/templates/login/denied.html @@ -4,6 +4,10 @@ {% load i18n %} {% load passbook_utils %} +{% block card_title %} +{% trans 'Permission denied' %} +{% endblock %} + {% block title %} {% trans 'Permission denied' %} {% endblock %} diff --git a/passbook/crypto/api.py b/passbook/crypto/api.py new file mode 100644 index 000000000..1a3f60806 --- /dev/null +++ b/passbook/crypto/api.py @@ -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 diff --git a/passbook/flows/apps.py b/passbook/flows/apps.py index b5eeb0666..5fd0b16fb 100644 --- a/passbook/flows/apps.py +++ b/passbook/flows/apps.py @@ -13,5 +13,4 @@ class PassbookFlowsConfig(AppConfig): verbose_name = "passbook Flows" def ready(self): - """Flow signals that clear the cache""" import_module("passbook.flows.signals") diff --git a/passbook/flows/migrations/0012_auto_20200830_1056.py b/passbook/flows/migrations/0012_auto_20200830_1056.py new file mode 100644 index 000000000..5fcbe28c2 --- /dev/null +++ b/passbook/flows/migrations/0012_auto_20200830_1056.py @@ -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=""), + ), + ] diff --git a/passbook/lib/config.py b/passbook/lib/config.py index d4d9cb39f..c5569b6a6 100644 --- a/passbook/lib/config.py +++ b/passbook/lib/config.py @@ -3,18 +3,17 @@ import os from collections.abc import Mapping from contextlib import contextmanager from glob import glob +from json import dumps from typing import Any, Dict from urllib.parse import urlparse import yaml from django.conf import ImproperlyConfigured from django.http import HttpRequest -from structlog import get_logger SEARCH_PATHS = ["passbook/lib/default.yml", "/etc/passbook/config.yml", ""] + glob( "/etc/passbook/config.d/*.yml", recursive=True ) -LOGGER = get_logger() ENV_PREFIX = "PASSBOOK" ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local") @@ -58,6 +57,13 @@ class ConfigLoader: self.update_from_file(env_file) self.update_from_env() + def _log(self, level: str, message: str, **kwargs): + """Custom Log method, we want to ensure ConfigLoader always logs JSON even when + 'structlog' or 'logging' hasn't been configured yet.""" + output = {"event": message, "level": level, "logger": self.__class__.__module__} + output.update(kwargs) + print(dumps(output)) + def update(self, root, updatee): """Recursively update dictionary""" for key, value in updatee.items(): @@ -82,12 +88,14 @@ class ConfigLoader: with open(path) as file: try: self.update(self.__config, yaml.safe_load(file)) - LOGGER.debug("Loaded config", file=path) + self._log("debug", "Loaded config", file=path) self.loaded_file.append(path) except yaml.YAMLError as exc: raise ImproperlyConfigured from exc except PermissionError as exc: - LOGGER.warning("Permission denied while reading file", path=path, error=exc) + self._log( + "warning", "Permission denied while reading file", path=path, error=exc + ) def update_from_dict(self, update: dict): """Update config from dict""" @@ -111,7 +119,7 @@ class ConfigLoader: current_obj[dot_parts[-1]] = value idx += 1 if idx > 0: - LOGGER.debug("Loaded environment variables", count=idx) + self._log("debug", "Loaded environment variables", count=idx) self.update(self.__config, outer) @contextmanager diff --git a/passbook/lib/default.yml b/passbook/lib/default.yml index fb8bea44f..77ff78a58 100644 --- a/passbook/lib/default.yml +++ b/passbook/lib/default.yml @@ -12,7 +12,7 @@ redis: message_queue_db: 1 debug: false -log_level: warning +log_level: info # Error reporting, sends stacktrace to sentry.beryju.org error_reporting: diff --git a/passbook/outposts/__init__.py b/passbook/outposts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/outposts/api.py b/passbook/outposts/api.py new file mode 100644 index 000000000..c0e82435f --- /dev/null +++ b/passbook/outposts/api.py @@ -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 diff --git a/passbook/outposts/apps.py b/passbook/outposts/apps.py new file mode 100644 index 000000000..61be6bab1 --- /dev/null +++ b/passbook/outposts/apps.py @@ -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") diff --git a/passbook/outposts/channels.py b/passbook/outposts/channels.py new file mode 100644 index 000000000..4a24da068 --- /dev/null +++ b/passbook/outposts/channels.py @@ -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) + ) + ) diff --git a/passbook/outposts/controllers/__init__.py b/passbook/outposts/controllers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/outposts/controllers/base.py b/passbook/outposts/controllers/base.py new file mode 100644 index 000000000..568eb4469 --- /dev/null +++ b/passbook/outposts/controllers/base.py @@ -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 diff --git a/passbook/outposts/controllers/compose.py b/passbook/outposts/controllers/compose.py new file mode 100644 index 000000000..e6706be5d --- /dev/null +++ b/passbook/outposts/controllers/compose.py @@ -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) diff --git a/passbook/outposts/controllers/kubernetes.py b/passbook/outposts/controllers/kubernetes.py new file mode 100644 index 000000000..081a61366 --- /dev/null +++ b/passbook/outposts/controllers/kubernetes.py @@ -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", + ) + ), + ), + ], + ) + ] + ), + ), + ), + ) diff --git a/passbook/outposts/forms.py b/passbook/outposts/forms.py new file mode 100644 index 000000000..cb9a1d3a9 --- /dev/null +++ b/passbook/outposts/forms.py @@ -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")} diff --git a/passbook/outposts/migrations/0001_initial.py b/passbook/outposts/migrations/0001_initial.py new file mode 100644 index 000000000..9f07d0195 --- /dev/null +++ b/passbook/outposts/migrations/0001_initial.py @@ -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")), + ], + ), + ] diff --git a/passbook/outposts/migrations/0002_auto_20200826_1306.py b/passbook/outposts/migrations/0002_auto_20200826_1306.py new file mode 100644 index 000000000..9687bf529 --- /dev/null +++ b/passbook/outposts/migrations/0002_auto_20200826_1306.py @@ -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"), + ), + ] diff --git a/passbook/outposts/migrations/0003_auto_20200827_2108.py b/passbook/outposts/migrations/0003_auto_20200827_2108.py new file mode 100644 index 000000000..34f4caa88 --- /dev/null +++ b/passbook/outposts/migrations/0003_auto_20200827_2108.py @@ -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 + ), + ), + ] diff --git a/passbook/outposts/migrations/0004_auto_20200830_1056.py b/passbook/outposts/migrations/0004_auto_20200830_1056.py new file mode 100644 index 000000000..e0185a02f --- /dev/null +++ b/passbook/outposts/migrations/0004_auto_20200830_1056.py @@ -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.", + ), + ), + ] diff --git a/passbook/outposts/migrations/__init__.py b/passbook/outposts/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/outposts/models.py b/passbook/outposts/models.py new file mode 100644 index 000000000..a82657a4e --- /dev/null +++ b/passbook/outposts/models.py @@ -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}" diff --git a/passbook/outposts/settings.py b/passbook/outposts/settings.py new file mode 100644 index 000000000..578777b56 --- /dev/null +++ b/passbook/outposts/settings.py @@ -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"}, + } +} diff --git a/passbook/outposts/signals.py b/passbook/outposts/signals.py new file mode 100644 index 000000000..0f97be49e --- /dev/null +++ b/passbook/outposts/signals.py @@ -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"}) diff --git a/passbook/outposts/tasks.py b/passbook/outposts/tasks.py new file mode 100644 index 000000000..8b4d33e47 --- /dev/null +++ b/passbook/outposts/tasks.py @@ -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() diff --git a/passbook/providers/proxy/templates/providers/proxy/setup_modal.html b/passbook/outposts/templates/outposts/setup_custom.html similarity index 95% rename from passbook/providers/proxy/templates/providers/proxy/setup_modal.html rename to passbook/outposts/templates/outposts/setup_custom.html index caa76fe06..e391a98fa 100644 --- a/passbook/providers/proxy/templates/providers/proxy/setup_modal.html +++ b/passbook/outposts/templates/outposts/setup_custom.html @@ -45,8 +45,8 @@

    {% trans 'Setup with Kubernetes' %}

    -

    {% trans 'Download the manifest to create the Gatekeeper deployment and service:' %}

    - {% trans 'Here' %} +

    {% trans 'Download the manifest to create the Proxy deployment and service:' %}

    + {% trans 'Here' %}

    {% trans 'Afterwards, add the following annotations to the Ingress you want to secure:' %}