Proxy v2 (#189)
This commit is contained in:
parent
14e47f3195
commit
268de20872
|
@ -1,7 +1,7 @@
|
|||
[run]
|
||||
source = passbook
|
||||
omit =
|
||||
*/wsgi.py
|
||||
*/asgi.py
|
||||
manage.py
|
||||
*/migrations/*
|
||||
*/apps.py
|
||||
|
|
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
|
@ -23,7 +23,7 @@ jobs:
|
|||
run: docker push beryju/passbook:0.9.0-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook:latest
|
||||
build-gatekeeper:
|
||||
build-proxy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
@ -34,16 +34,16 @@ jobs:
|
|||
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
||||
- name: Building Docker Image
|
||||
run: |
|
||||
cd gatekeeper
|
||||
cd proxy
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/passbook-gatekeeper:0.9.0-stable \
|
||||
-t beryju/passbook-gatekeeper:latest \
|
||||
-t beryju/passbook-proxy:0.9.0-stable \
|
||||
-t beryju/passbook-proxy:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-gatekeeper:0.9.0-stable
|
||||
run: docker push beryju/passbook-proxy:0.9.0-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-gatekeeper:latest
|
||||
run: docker push beryju/passbook-proxy:latest
|
||||
build-static:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
|
|
|
@ -17,14 +17,16 @@ COPY --from=locker /app/requirements-dev.txt /app/
|
|||
WORKDIR /app/
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends postgresql-client-11 && \
|
||||
apt-get install -y --no-install-recommends postgresql-client-11 build-essential && \
|
||||
rm -rf /var/lib/apt/ && \
|
||||
pip install -r requirements.txt --no-cache-dir && \
|
||||
apt-get remove --purge -y build-essential && \
|
||||
apt-get autoremove --purge && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /app passbook
|
||||
|
||||
COPY ./passbook/ /app/passbook
|
||||
COPY ./manage.py /app/
|
||||
COPY ./docker/uwsgi.ini /app/
|
||||
COPY ./docker/gunicorn.conf.py /app/
|
||||
COPY ./docker/bootstrap.sh /bootstrap.sh
|
||||
COPY ./docker/wait_for_db.py /app/wait_for_db.py
|
||||
|
||||
|
|
20
Makefile
Normal file
20
Makefile
Normal file
|
@ -0,0 +1,20 @@
|
|||
all: lint-fix lint coverage gen
|
||||
|
||||
coverage:
|
||||
coverage run --concurrency=multiprocessing manage.py test passbook --failfast
|
||||
coverage combine
|
||||
coverage html
|
||||
coverage report
|
||||
|
||||
lint-fix:
|
||||
isort -rc .
|
||||
black .
|
||||
|
||||
lint:
|
||||
pyright
|
||||
bandit -r .
|
||||
pylint passbook
|
||||
prospector
|
||||
|
||||
gen: coverage
|
||||
./manage.py generate_swagger -o swagger.yaml -f yaml
|
6
Pipfile
6
Pipfile
|
@ -28,7 +28,8 @@ packaging = "*"
|
|||
psycopg2-binary = "*"
|
||||
pycryptodome = "*"
|
||||
pyjwkest = "*"
|
||||
pyuwsgi = "*"
|
||||
uvicorn = "*"
|
||||
gunicorn = "*"
|
||||
pyyaml = "*"
|
||||
qrcode = "*"
|
||||
requests-oauthlib = "*"
|
||||
|
@ -39,6 +40,9 @@ structlog = "*"
|
|||
swagger-spec-validator = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
dacite = "*"
|
||||
channels = "*"
|
||||
channels-redis = "*"
|
||||
kubernetes = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
|
|
488
Pipfile.lock
generated
488
Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "8f099b73d5993a0693261bf3d2b0e696d4f4d7ddd69a10d3db8ffe59a8ebd805"
|
||||
"sha256": "a798bbd0b97857cac136c1743b8d6ad8bf8c3d95e2760c71d324bb2a7f47f678"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -16,11 +16,19 @@
|
|||
]
|
||||
},
|
||||
"default": {
|
||||
"aioredis": {
|
||||
"hashes": [
|
||||
"sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a",
|
||||
"sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"
|
||||
],
|
||||
"version": "==1.3.1"
|
||||
},
|
||||
"amqp": {
|
||||
"hashes": [
|
||||
"sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21",
|
||||
"sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.6.1"
|
||||
},
|
||||
"asgiref": {
|
||||
|
@ -28,15 +36,40 @@
|
|||
"sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
|
||||
"sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.2.10"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||
],
|
||||
"markers": "python_full_version >= '3.5.3'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a",
|
||||
"sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.1.0"
|
||||
},
|
||||
"autobahn": {
|
||||
"hashes": [
|
||||
"sha256:24ce276d313e84d68241c3aef30d484f352b90a40168981b3640312c821df77b",
|
||||
"sha256:86bbce30cdd407137c57670993a8f9bfdfe3f8e994b889181d85e844d5aa8dfb"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==20.7.1"
|
||||
},
|
||||
"automat": {
|
||||
"hashes": [
|
||||
"sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33",
|
||||
"sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111"
|
||||
],
|
||||
"version": "==20.2.0"
|
||||
},
|
||||
"billiard": {
|
||||
"hashes": [
|
||||
"sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede",
|
||||
|
@ -46,17 +79,26 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:b240ac281de363e25a8e1a4c862559d6a056d98dcb9f487fc94d73c6f6599dfc"
|
||||
"sha256:4196b418598851ffd10cf9d1606694673cbfeca4ddf8b25d4e50addbd2fc60bf",
|
||||
"sha256:69ad8f2184979e223e12ee3071674fdf910983cf9f4d6f34f7ec407b089064b5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.14.53"
|
||||
"version": "==1.14.54"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:7e0272ceeb7747ed259a392e8d7b624cfd037085a8c59ef2b9f8916e7c556267",
|
||||
"sha256:d37a83ac23257c85c48b74ab81173980234f8fc078e7a9d312d0ee7d057f90e6"
|
||||
"sha256:6fe05837646447d61acdaf1e3401b92cd9309f00b19c577a50d0ade7735a3403",
|
||||
"sha256:9e493a21e6a8d45c631eb2952ae8e1d0a31b9984546d4268ea10c0c33e2435ce"
|
||||
],
|
||||
"version": "==1.17.53"
|
||||
"version": "==1.17.54"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
"sha256:513d4ff98dd27f85743a8dc0e92f55ddb1b49e060c2d5961512855cda2c01a98",
|
||||
"sha256:bbaa39c3dede00175df2dc2b03d0cf18dd2d32a7de7beb68072d13043c9edb20"
|
||||
],
|
||||
"markers": "python_version ~= '3.5'",
|
||||
"version": "==4.1.1"
|
||||
},
|
||||
"celery": {
|
||||
"hashes": [
|
||||
|
@ -106,6 +148,22 @@
|
|||
],
|
||||
"version": "==1.14.2"
|
||||
},
|
||||
"channels": {
|
||||
"hashes": [
|
||||
"sha256:08e756406d7165cb32f6fc3090c0643f41ca9f7e0f7fada0b31194662f20f414",
|
||||
"sha256:80a5ad1962ae039a3dcc0a5cb5212413e66e2f11ad9e9db8004834436daf3400"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.4.0"
|
||||
},
|
||||
"channels-redis": {
|
||||
"hashes": [
|
||||
"sha256:b4bcee949032cd838abdffd10da056930fca1a5a7ebc52139f8537aa622ac8d5",
|
||||
"sha256:be7c14526ab924a091a66ad72a8be57a34900440b1126d520ac7742c0e2add03"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
|
@ -113,6 +171,21 @@
|
|||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"constantly": {
|
||||
"hashes": [
|
||||
"sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35",
|
||||
"sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"
|
||||
],
|
||||
"version": "==15.1.0"
|
||||
},
|
||||
"coreapi": {
|
||||
"hashes": [
|
||||
"sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb",
|
||||
|
@ -159,6 +232,13 @@
|
|||
"index": "pypi",
|
||||
"version": "==1.5.1"
|
||||
},
|
||||
"daphne": {
|
||||
"hashes": [
|
||||
"sha256:1ca46d7419103958bbc9576fb7ba3b25b053006e22058bc97084ee1a7d44f4ba",
|
||||
"sha256:aa64840015709bbc9daa3c4464a4a4d437937d6cda10a9b51e913eb319272553"
|
||||
],
|
||||
"version": "==2.5.0"
|
||||
},
|
||||
"defusedxml": {
|
||||
"hashes": [
|
||||
"sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93",
|
||||
|
@ -265,6 +345,7 @@
|
|||
"sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32",
|
||||
"sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.11.1"
|
||||
},
|
||||
"djangorestframework-guardian": {
|
||||
|
@ -281,6 +362,7 @@
|
|||
"sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827",
|
||||
"sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.15.2"
|
||||
},
|
||||
"drf-yasg": {
|
||||
|
@ -310,8 +392,109 @@
|
|||
"hashes": [
|
||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"google-auth": {
|
||||
"hashes": [
|
||||
"sha256:982e1f82cace752134660b4c0ff660761b32146a55abb3ad6d225529012af87c",
|
||||
"sha256:f2498ad9cac3d2942d6c509ba18c4639656b366681881a1805f44f2a0c2d46f1"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.21.0"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
"sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
|
||||
"sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.0.4"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1",
|
||||
"sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"
|
||||
],
|
||||
"version": "==0.9.0"
|
||||
},
|
||||
"hiredis": {
|
||||
"hashes": [
|
||||
"sha256:06a039208f83744a702279b894c8cf24c14fd63c59cd917dcde168b79eef0680",
|
||||
"sha256:0a909bf501459062aa1552be1461456518f367379fdc9fdb1f2ca5e4a1fdd7c0",
|
||||
"sha256:18402d9e54fb278cb9a8c638df6f1550aca36a009d47ecf5aa263a38600f35b0",
|
||||
"sha256:1e4cbbc3858ec7e680006e5ca590d89a5e083235988f26a004acf7244389ac01",
|
||||
"sha256:23344e3c2177baf6975fbfa361ed92eb7d36d08f454636e5054b3faa7c2aff8a",
|
||||
"sha256:289b31885b4996ce04cadfd5fc03d034dce8e2a8234479f7c9e23b9e245db06b",
|
||||
"sha256:2c1c570ae7bf1bab304f29427e2475fe1856814312c4a1cf1cd0ee133f07a3c6",
|
||||
"sha256:2c227c0ed371771ffda256034427320870e8ea2e4fd0c0a618c766e7c49aad73",
|
||||
"sha256:3bb9b63d319402cead8bbd9dd55dca3b667d2997e9a0d8a1f9b6cc274db4baee",
|
||||
"sha256:3ef2183de67b59930d2db8b8e8d4d58e00a50fcc5e92f4f678f6eed7a1c72d55",
|
||||
"sha256:43b8ed3dbfd9171e44c554cb4acf4ee4505caa84c5e341858b50ea27dd2b6e12",
|
||||
"sha256:47bcf3c5e6c1e87ceb86cdda2ee983fa0fe56a999e6185099b3c93a223f2fa9b",
|
||||
"sha256:5263db1e2e1e8ae30500cdd75a979ff99dcc184201e6b4b820d0de74834d2323",
|
||||
"sha256:5b1451727f02e7acbdf6aae4e06d75f66ee82966ff9114550381c3271a90f56c",
|
||||
"sha256:6996883a8a6ff9117cbb3d6f5b0dcbbae6fb9e31e1a3e4e2f95e0214d9a1c655",
|
||||
"sha256:6c96f64a54f030366657a54bb90b3093afc9c16c8e0dfa29fc0d6dbe169103a5",
|
||||
"sha256:7332d5c3e35154cd234fd79573736ddcf7a0ade7a986db35b6196b9171493e75",
|
||||
"sha256:7885b6f32c4a898e825bb7f56f36a02781ac4a951c63e4169f0afcf9c8c30dfb",
|
||||
"sha256:7b0f63f10a166583ab744a58baad04e0f52cfea1ac27bfa1b0c21a48d1003c23",
|
||||
"sha256:819f95d4eba3f9e484dd115ab7ab72845cf766b84286a00d4ecf76d33f1edca1",
|
||||
"sha256:8968eeaa4d37a38f8ca1f9dbe53526b69628edc9c42229a5b2f56d98bb828c1f",
|
||||
"sha256:89ebf69cb19a33d625db72d2ac589d26e936b8f7628531269accf4a3196e7872",
|
||||
"sha256:8daecd778c1da45b8bd54fd41ffcd471a86beed3d8e57a43acf7a8d63bba4058",
|
||||
"sha256:955ba8ea73cf3ed8bd2f963b4cb9f8f0dcb27becd2f4b3dd536fd24c45533454",
|
||||
"sha256:964f18a59f5a64c0170f684c417f4fe3e695a536612e13074c4dd5d1c6d7c882",
|
||||
"sha256:969843fbdfbf56cdb71da6f0bdf50f9985b8b8aeb630102945306cf10a9c6af2",
|
||||
"sha256:996021ef33e0f50b97ff2d6b5f422a0fe5577de21a8873b58a779a5ddd1c3132",
|
||||
"sha256:9e9c9078a7ce07e6fce366bd818be89365a35d2e4b163268f0ca9ba7e13bb2f6",
|
||||
"sha256:a04901757cb0fb0f5602ac11dda48f5510f94372144d06c2563ba56c480b467c",
|
||||
"sha256:a7bf1492429f18d205f3a818da3ff1f242f60aa59006e53dee00b4ef592a3363",
|
||||
"sha256:aa0af2deb166a5e26e0d554b824605e660039b161e37ed4f01b8d04beec184f3",
|
||||
"sha256:abfb15a6a7822f0fae681785cb38860e7a2cb1616a708d53df557b3d76c5bfd4",
|
||||
"sha256:b253fe4df2afea4dfa6b1fa8c5fef212aff8bcaaeb4207e81eed05cb5e4a7919",
|
||||
"sha256:b27f082f47d23cffc4cf1388b84fdc45c4ef6015f906cd7e0d988d9e35d36349",
|
||||
"sha256:b33aea449e7f46738811fbc6f0b3177c6777a572207412bbbf6f525ffed001ae",
|
||||
"sha256:b44f9421c4505c548435244d74037618f452844c5d3c67719d8a55e2613549da",
|
||||
"sha256:bcc371151d1512201d0214c36c0c150b1dc64f19c2b1a8c9cb1d7c7c15ebd93f",
|
||||
"sha256:c2851deeabd96d3f6283e9c6b26e0bfed4de2dc6fb15edf913e78b79fc5909ed",
|
||||
"sha256:cdfd501c7ac5b198c15df800a3a34c38345f5182e5f80770caf362bccca65628",
|
||||
"sha256:d2c0caffa47606d6d7c8af94ba42547bd2a441f06c74fd90a1ffe328524a6c64",
|
||||
"sha256:dcb2db95e629962db5a355047fb8aefb012df6c8ae608930d391619dbd96fd86",
|
||||
"sha256:e0eeb9c112fec2031927a1745788a181d0eecbacbed941fc5c4f7bc3f7b273bf",
|
||||
"sha256:e154891263306200260d7f3051982774d7b9ef35af3509d5adbbe539afd2610c",
|
||||
"sha256:e2e023a42dcbab8ed31f97c2bcdb980b7fbe0ada34037d87ba9d799664b58ded",
|
||||
"sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390",
|
||||
"sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"httptools": {
|
||||
"hashes": [
|
||||
"sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be",
|
||||
"sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d",
|
||||
"sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce",
|
||||
"sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2",
|
||||
"sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6",
|
||||
"sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f",
|
||||
"sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009",
|
||||
"sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce",
|
||||
"sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a",
|
||||
"sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c",
|
||||
"sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4",
|
||||
"sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"
|
||||
],
|
||||
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'",
|
||||
"version": "==0.1.1"
|
||||
},
|
||||
"hyperlink": {
|
||||
"hashes": [
|
||||
"sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af",
|
||||
"sha256:c528d405766f15a2c536230de7e160b65a08e20264d8891b3eb03307b0df3c63"
|
||||
],
|
||||
"version": "==20.0.1"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
|
@ -319,11 +502,19 @@
|
|||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"incremental": {
|
||||
"hashes": [
|
||||
"sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f",
|
||||
"sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3"
|
||||
],
|
||||
"version": "==17.5.0"
|
||||
},
|
||||
"inflection": {
|
||||
"hashes": [
|
||||
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
||||
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"itypes": {
|
||||
|
@ -338,6 +529,7 @@
|
|||
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
||||
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.11.2"
|
||||
},
|
||||
"jmespath": {
|
||||
|
@ -345,6 +537,7 @@
|
|||
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
|
||||
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"jsonschema": {
|
||||
|
@ -359,11 +552,23 @@
|
|||
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
|
||||
"sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==4.6.11"
|
||||
},
|
||||
"kubernetes": {
|
||||
"hashes": [
|
||||
"sha256:1a2472f8b01bc6aa87e3a34781f859bded5a5c8ff791a53d889a8bd6cc550430",
|
||||
"sha256:4af81201520977139a143f96123fb789fa351879df37f122916b9b6ed050bbaf"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==11.0.0"
|
||||
},
|
||||
"ldap3": {
|
||||
"hashes": [
|
||||
"sha256:59d1adcd5ead263387039e2a37d7cd772a2006b1cdb3ecfcbaab5192a601c515",
|
||||
"sha256:7abbb3e5f4522114e0230ec175b60ae968b938d1f8a7d8bce7789f78d871fb9f",
|
||||
"sha256:b399c39e80b6459e349b33fbe9787c1bcbf86de05994d41806a05c06f3e7574d",
|
||||
"sha256:bdaf568cd30fc0006c8bb4f5e6014554afeb0c4bbea1677de9706e278a4057e7",
|
||||
"sha256:df27407f4991f25bd669b5bb1bc8cb9ddf44a3e713ff6b3afeb3b3c26502f88f"
|
||||
],
|
||||
"index": "pypi",
|
||||
|
@ -442,13 +647,38 @@
|
|||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"msgpack": {
|
||||
"hashes": [
|
||||
"sha256:002a0d813e1f7b60da599bdf969e632074f9eec1b96cbed8fb0973a63160a408",
|
||||
"sha256:25b3bc3190f3d9d965b818123b7752c5dfb953f0d774b454fd206c18fe384fb8",
|
||||
"sha256:271b489499a43af001a2e42f42d876bb98ccaa7e20512ff37ca78c8e12e68f84",
|
||||
"sha256:39c54fdebf5fa4dda733369012c59e7d085ebdfe35b6cf648f09d16708f1be5d",
|
||||
"sha256:4233b7f86c1208190c78a525cd3828ca1623359ef48f78a6fea4b91bb995775a",
|
||||
"sha256:5bea44181fc8e18eed1d0cd76e355073f00ce232ff9653a0ae88cb7d9e643322",
|
||||
"sha256:5dba6d074fac9b24f29aaf1d2d032306c27f04187651511257e7831733293ec2",
|
||||
"sha256:7a22c965588baeb07242cb561b63f309db27a07382825fc98aecaf0827c1538e",
|
||||
"sha256:908944e3f038bca67fcfedb7845c4a257c7749bf9818632586b53bcf06ba4b97",
|
||||
"sha256:9534d5cc480d4aff720233411a1f765be90885750b07df772380b34c10ecb5c0",
|
||||
"sha256:aa5c057eab4f40ec47ea6f5a9825846be2ff6bf34102c560bad5cad5a677c5be",
|
||||
"sha256:b3758dfd3423e358bbb18a7cccd1c74228dffa7a697e5be6cb9535de625c0dbf",
|
||||
"sha256:c901e8058dd6653307906c5f157f26ed09eb94a850dddd989621098d347926ab",
|
||||
"sha256:cec8bf10981ed70998d98431cd814db0ecf3384e6b113366e7f36af71a0fca08",
|
||||
"sha256:db685187a415f51d6b937257474ca72199f393dad89534ebbdd7d7a3b000080e",
|
||||
"sha256:e35b051077fc2f3ce12e7c6a34cf309680c63a842db3a0616ea6ed25ad20d272",
|
||||
"sha256:e7bbdd8e2b277b77782f3ce34734b0dfde6cbe94ddb74de8d733d603c7f9e2b1",
|
||||
"sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140"
|
||||
],
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"oauthlib": {
|
||||
"hashes": [
|
||||
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
|
||||
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"packaging": {
|
||||
|
@ -504,15 +734,37 @@
|
|||
},
|
||||
"pyasn1": {
|
||||
"hashes": [
|
||||
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
|
||||
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
|
||||
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
|
||||
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
|
||||
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
|
||||
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
|
||||
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
|
||||
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
|
||||
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
|
||||
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
|
||||
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
|
||||
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
|
||||
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
|
||||
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
|
||||
],
|
||||
"version": "==0.4.8"
|
||||
},
|
||||
"pyasn1-modules": {
|
||||
"hashes": [
|
||||
"sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8",
|
||||
"sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",
|
||||
"sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",
|
||||
"sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",
|
||||
"sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4",
|
||||
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
|
||||
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"
|
||||
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",
|
||||
"sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",
|
||||
"sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
|
||||
"sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd",
|
||||
"sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",
|
||||
"sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",
|
||||
"sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"
|
||||
],
|
||||
"version": "==0.2.8"
|
||||
},
|
||||
|
@ -521,6 +773,7 @@
|
|||
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
|
||||
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.20"
|
||||
},
|
||||
"pycryptodome": {
|
||||
|
@ -592,8 +845,17 @@
|
|||
"sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46",
|
||||
"sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.9.8"
|
||||
},
|
||||
"pyhamcrest": {
|
||||
"hashes": [
|
||||
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
|
||||
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.0.2"
|
||||
},
|
||||
"pyjwkest": {
|
||||
"hashes": [
|
||||
"sha256:5560fd5ba08655f29ff6ad1df1e15dc05abc9d976fcbcec8d2b5167f49b70222"
|
||||
|
@ -613,6 +875,7 @@
|
|||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pyrsistent": {
|
||||
|
@ -626,6 +889,7 @@
|
|||
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
||||
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"pytz": {
|
||||
|
@ -635,24 +899,6 @@
|
|||
],
|
||||
"version": "==2020.1"
|
||||
},
|
||||
"pyuwsgi": {
|
||||
"hashes": [
|
||||
"sha256:1a4dd8d99b8497f109755e09484b0bd2aeaa533f7621e7c7e2a120a72111219d",
|
||||
"sha256:206937deaebbac5c87692657c3151a5a9d40ecbc9b051b94154205c50a48e963",
|
||||
"sha256:2cf35d9145208cc7c96464d688caa3de745bfc969e1a1ae23cb046fc10b0ac7e",
|
||||
"sha256:3ab84a168633eeb55847d59475d86e9078d913d190c2a1aed804c562a10301a3",
|
||||
"sha256:430406d1bcf288a87f14fde51c66877eaf5e98516838a1c6f761af5d814936fc",
|
||||
"sha256:72be25ce7aa86c5616c59d12c2961b938e7bde47b7ff6a996ff83b89f7c5cd27",
|
||||
"sha256:aa4d615de430e2066a1c76d9cc2a70abf2dfc703a82c21aee625b445866f2c3b",
|
||||
"sha256:aadd231256a672cf4342ef9fb976051949e4d5b616195e696bcb7b8a9c07789e",
|
||||
"sha256:b15ee6a7759b0465786d856334b8231d882deda5291cf243be6a343a8f3ef910",
|
||||
"sha256:bd1d0a8d4cb87eb63417a72e6b1bac47053f9b0be550adc6d2a375f4cbaa22f0",
|
||||
"sha256:d5787779ec24b67ac8898be9dc2b2b4e35f17d79f14361f6cf303d6283a848f2",
|
||||
"sha256:ecfae85d6504e0ecbba100a795032a88ce8f110b62b93243f2df1bd116eca67f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.19.1"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
|
||||
|
@ -683,6 +929,7 @@
|
|||
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
|
||||
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==3.5.3"
|
||||
},
|
||||
"requests": {
|
||||
|
@ -690,16 +937,26 @@
|
|||
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.24.0"
|
||||
},
|
||||
"requests-oauthlib": {
|
||||
"hashes": [
|
||||
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
|
||||
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
|
||||
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
|
||||
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"rsa": {
|
||||
"hashes": [
|
||||
"sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa",
|
||||
"sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==4.6"
|
||||
},
|
||||
"ruamel.yaml": {
|
||||
"hashes": [
|
||||
"sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b",
|
||||
|
@ -729,7 +986,7 @@
|
|||
"sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad",
|
||||
"sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e"
|
||||
],
|
||||
"markers": "platform_python_implementation == 'CPython' and python_version < '3.9'",
|
||||
"markers": "python_version < '3.9' and platform_python_implementation == 'CPython'",
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"s3transfer": {
|
||||
|
@ -741,11 +998,11 @@
|
|||
},
|
||||
"sentry-sdk": {
|
||||
"hashes": [
|
||||
"sha256:5b884a391da04696c1d81d636d2ad728fd838370db1acdfda3acbad1fe5be830",
|
||||
"sha256:bbfe5633aee4dacb53d79d303ab6bfacf1749fb717750c112fb1658e5accce0d"
|
||||
"sha256:0af429c221670e602f960fca85ca3f607c85510a91f11e8be8f742a978127f78",
|
||||
"sha256:a088a1054673c6a19ea590045c871c38da029ef743b61a07bfee95e9f3c060f7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.17.2"
|
||||
"version": "==0.17.3"
|
||||
},
|
||||
"service-identity": {
|
||||
"hashes": [
|
||||
|
@ -768,6 +1025,7 @@
|
|||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"sqlparse": {
|
||||
|
@ -775,6 +1033,7 @@
|
|||
"sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
|
||||
"sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.3.1"
|
||||
},
|
||||
"structlog": {
|
||||
|
@ -793,11 +1052,52 @@
|
|||
"index": "pypi",
|
||||
"version": "==2.7.3"
|
||||
},
|
||||
"twisted": {
|
||||
"extras": [
|
||||
"tls"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:040eb6641125d2a9a09cf198ec7b83dd8858c6f51f6770325ed9959c00f5098f",
|
||||
"sha256:147780b8caf21ba2aef3688628eaf13d7e7fe02a86747cd54bfaf2140538f042",
|
||||
"sha256:158ddb80719a4813d292293ac44ba41d8b56555ed009d90994a278237ee63d2c",
|
||||
"sha256:2182000d6ffc05d269e6c03bfcec8b57e20259ca1086180edaedec3f1e689292",
|
||||
"sha256:25ffcf37944bdad4a99981bc74006d735a678d2b5c193781254fbbb6d69e3b22",
|
||||
"sha256:3281d9ce889f7b21bdb73658e887141aa45a102baf3b2320eafcfba954fcefec",
|
||||
"sha256:356e8d8dd3590e790e3dba4db139eb8a17aca64b46629c622e1b1597a4a92478",
|
||||
"sha256:70952c56e4965b9f53b180daecf20a9595cf22b8d0935cd3bd664c90273c3ab2",
|
||||
"sha256:7408c6635ee1b96587289283ebe90ee15dbf9614b05857b446055116bc822d29",
|
||||
"sha256:7c547fd0215db9da8a1bc23182b309e84a232364cc26d829e9ee196ce840b114",
|
||||
"sha256:894f6f3cfa57a15ea0d0714e4283913a5f2511dbd18653dd148eba53b3919797",
|
||||
"sha256:94ac3d55a58c90e2075c5fe1853f2aa3892b73e3bf56395f743aefde8605eeaa",
|
||||
"sha256:a58e61a2a01e5bcbe3b575c0099a2bcb8d70a75b1a087338e0c48dd6e01a5f15",
|
||||
"sha256:c09c47ff9750a8e3aa60ad169c4b95006d455a29b80ad0901f031a103b2991cd",
|
||||
"sha256:ca3a0b8c9110800e576d89b5337373e52018b41069bc879f12fa42b7eb2d0274",
|
||||
"sha256:cd1dc5c85b58494138a3917752b54bb1daa0045d234b7c132c37a61d5483ebad",
|
||||
"sha256:cdbc4c7f0cd7a2218b575844e970f05a1be1861c607b0e048c9bceca0c4d42f7",
|
||||
"sha256:d267125cc0f1e8a0eed6319ba4ac7477da9b78a535601c49ecd20c875576433a",
|
||||
"sha256:d72c55b5d56e176563b91d11952d13b01af8725c623e498db5507b6614fc1e10",
|
||||
"sha256:d95803193561a243cb0401b0567c6b7987d3f2a67046770e1dccd1c9e49a9780",
|
||||
"sha256:e92703bed0cc21d6cb5c61d66922b3b1564015ca8a51325bd164a5e33798d504",
|
||||
"sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467",
|
||||
"sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==20.3.0"
|
||||
},
|
||||
"txaio": {
|
||||
"hashes": [
|
||||
"sha256:17938f2bca4a9cabce61346758e482ca4e600160cbc28e861493eac74a19539d",
|
||||
"sha256:38a469daf93c37e5527cb062653d6393ae11663147c42fab7ddc3f6d00d434ae"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==20.4.1"
|
||||
},
|
||||
"uritemplate": {
|
||||
"hashes": [
|
||||
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
|
||||
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"urllib3": {
|
||||
|
@ -809,15 +1109,119 @@
|
|||
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": null,
|
||||
"version": "==1.25.10"
|
||||
},
|
||||
"uvicorn": {
|
||||
"hashes": [
|
||||
"sha256:46a83e371f37ea7ff29577d00015f02c942410288fb57def6440f2653fff1d26",
|
||||
"sha256:4b70ddb4c1946e39db9f3082d53e323dfd50634b95fd83625d778729ef1730ef"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.11.8"
|
||||
},
|
||||
"uvloop": {
|
||||
"hashes": [
|
||||
"sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd",
|
||||
"sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e",
|
||||
"sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09",
|
||||
"sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726",
|
||||
"sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891",
|
||||
"sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7",
|
||||
"sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5",
|
||||
"sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95",
|
||||
"sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"
|
||||
],
|
||||
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'",
|
||||
"version": "==0.14.0"
|
||||
},
|
||||
"vine": {
|
||||
"hashes": [
|
||||
"sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87",
|
||||
"sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"websocket-client": {
|
||||
"hashes": [
|
||||
"sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549",
|
||||
"sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010"
|
||||
],
|
||||
"version": "==0.57.0"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
"sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5",
|
||||
"sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5",
|
||||
"sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308",
|
||||
"sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb",
|
||||
"sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a",
|
||||
"sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c",
|
||||
"sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170",
|
||||
"sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422",
|
||||
"sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8",
|
||||
"sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485",
|
||||
"sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f",
|
||||
"sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8",
|
||||
"sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc",
|
||||
"sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779",
|
||||
"sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989",
|
||||
"sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1",
|
||||
"sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092",
|
||||
"sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824",
|
||||
"sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d",
|
||||
"sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55",
|
||||
"sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36",
|
||||
"sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==8.1"
|
||||
},
|
||||
"zope.interface": {
|
||||
"hashes": [
|
||||
"sha256:0103cba5ed09f27d2e3de7e48bb320338592e2fabc5ce1432cf33808eb2dfd8b",
|
||||
"sha256:14415d6979356629f1c386c8c4249b4d0082f2ea7f75871ebad2e29584bd16c5",
|
||||
"sha256:1ae4693ccee94c6e0c88a4568fb3b34af8871c60f5ba30cf9f94977ed0e53ddd",
|
||||
"sha256:1b87ed2dc05cb835138f6a6e3595593fea3564d712cb2eb2de963a41fd35758c",
|
||||
"sha256:269b27f60bcf45438e8683269f8ecd1235fa13e5411de93dae3b9ee4fe7f7bc7",
|
||||
"sha256:27d287e61639d692563d9dab76bafe071fbeb26818dd6a32a0022f3f7ca884b5",
|
||||
"sha256:39106649c3082972106f930766ae23d1464a73b7d30b3698c986f74bf1256a34",
|
||||
"sha256:40e4c42bd27ed3c11b2c983fecfb03356fae1209de10686d03c02c8696a1d90e",
|
||||
"sha256:461d4339b3b8f3335d7e2c90ce335eb275488c587b61aca4b305196dde2ff086",
|
||||
"sha256:4f98f70328bc788c86a6a1a8a14b0ea979f81ae6015dd6c72978f1feff70ecda",
|
||||
"sha256:558a20a0845d1a5dc6ff87cd0f63d7dac982d7c3be05d2ffb6322a87c17fa286",
|
||||
"sha256:562dccd37acec149458c1791da459f130c6cf8902c94c93b8d47c6337b9fb826",
|
||||
"sha256:5e86c66a6dea8ab6152e83b0facc856dc4d435fe0f872f01d66ce0a2131b7f1d",
|
||||
"sha256:60a207efcd8c11d6bbeb7862e33418fba4e4ad79846d88d160d7231fcb42a5ee",
|
||||
"sha256:645a7092b77fdbc3f68d3cc98f9d3e71510e419f54019d6e282328c0dd140dcd",
|
||||
"sha256:6874367586c020705a44eecdad5d6b587c64b892e34305bb6ed87c9bbe22a5e9",
|
||||
"sha256:74bf0a4f9091131de09286f9a605db449840e313753949fe07c8d0fe7659ad1e",
|
||||
"sha256:7b726194f938791a6691c7592c8b9e805fc6d1b9632a833b9c0640828cd49cbc",
|
||||
"sha256:8149ded7f90154fdc1a40e0c8975df58041a6f693b8f7edcd9348484e9dc17fe",
|
||||
"sha256:8cccf7057c7d19064a9e27660f5aec4e5c4001ffcf653a47531bde19b5aa2a8a",
|
||||
"sha256:911714b08b63d155f9c948da2b5534b223a1a4fc50bb67139ab68b277c938578",
|
||||
"sha256:a5f8f85986197d1dd6444763c4a15c991bfed86d835a1f6f7d476f7198d5f56a",
|
||||
"sha256:a744132d0abaa854d1aad50ba9bc64e79c6f835b3e92521db4235a1991176813",
|
||||
"sha256:af2c14efc0bb0e91af63d00080ccc067866fb8cbbaca2b0438ab4105f5e0f08d",
|
||||
"sha256:b054eb0a8aa712c8e9030065a59b5e6a5cf0746ecdb5f087cca5ec7685690c19",
|
||||
"sha256:b0becb75418f8a130e9d465e718316cd17c7a8acce6fe8fe07adc72762bee425",
|
||||
"sha256:b1d2ed1cbda2ae107283befd9284e650d840f8f7568cb9060b5466d25dc48975",
|
||||
"sha256:ba4261c8ad00b49d48bbb3b5af388bb7576edfc0ca50a49c11dcb77caa1d897e",
|
||||
"sha256:d1fe9d7d09bb07228650903d6a9dc48ea649e3b8c69b1d263419cc722b3938e8",
|
||||
"sha256:d7804f6a71fc2dda888ef2de266727ec2f3915373d5a785ed4ddc603bbc91e08",
|
||||
"sha256:da2844fba024dd58eaa712561da47dcd1e7ad544a257482392472eae1c86d5e5",
|
||||
"sha256:dcefc97d1daf8d55199420e9162ab584ed0893a109f45e438b9794ced44c9fd0",
|
||||
"sha256:dd98c436a1fc56f48c70882cc243df89ad036210d871c7427dc164b31500dc11",
|
||||
"sha256:e74671e43ed4569fbd7989e5eecc7d06dc134b571872ab1d5a88f4a123814e9f",
|
||||
"sha256:eb9b92f456ff3ec746cd4935b73c1117538d6124b8617bc0fe6fda0b3816e345",
|
||||
"sha256:ebb4e637a1fb861c34e48a00d03cffa9234f42bef923aec44e5625ffb9a8e8f9",
|
||||
"sha256:ef739fe89e7f43fb6494a43b1878a36273e5924869ba1d866f752c5812ae8d58",
|
||||
"sha256:f40db0e02a8157d2b90857c24d89b6310f9b6c3642369852cdc3b5ac49b92afc",
|
||||
"sha256:f68bf937f113b88c866d090fea0bc52a098695173fc613b055a17ff0cf9683b6",
|
||||
"sha256:fb55c182a3f7b84c1a2d6de5fa7b1a05d4660d866b91dbf8d74549c57a1499e8"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==5.1.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
|
@ -833,6 +1237,7 @@
|
|||
"sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
|
||||
"sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.2.10"
|
||||
},
|
||||
"astroid": {
|
||||
|
@ -840,6 +1245,7 @@
|
|||
"sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1",
|
||||
"sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.4.1"
|
||||
},
|
||||
"attrs": {
|
||||
|
@ -847,6 +1253,7 @@
|
|||
"sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a",
|
||||
"sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.1.0"
|
||||
},
|
||||
"autopep8": {
|
||||
|
@ -877,6 +1284,7 @@
|
|||
"sha256:477f0e18a0d58e50bb3dbc9af7fcda464fd0ebfc7a6151d8888602d7153171a0",
|
||||
"sha256:cd4f3a231305e405ed8944d8ff35bd742d9bc740ad62f483bd0ca21ce7131984"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"bumpversion": {
|
||||
|
@ -906,6 +1314,7 @@
|
|||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"colorama": {
|
||||
|
@ -992,6 +1401,7 @@
|
|||
"sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c",
|
||||
"sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.8.3"
|
||||
},
|
||||
"flake8-polyfill": {
|
||||
|
@ -1006,6 +1416,7 @@
|
|||
"sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac",
|
||||
"sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"
|
||||
],
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==4.0.5"
|
||||
},
|
||||
"gitpython": {
|
||||
|
@ -1013,6 +1424,7 @@
|
|||
"sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858",
|
||||
"sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"
|
||||
],
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==3.1.7"
|
||||
},
|
||||
"idna": {
|
||||
|
@ -1027,6 +1439,7 @@
|
|||
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
|
||||
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==4.3.21"
|
||||
},
|
||||
"lazy-object-proxy": {
|
||||
|
@ -1053,6 +1466,7 @@
|
|||
"sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4",
|
||||
"sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"mccabe": {
|
||||
|
@ -1074,6 +1488,7 @@
|
|||
"sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea",
|
||||
"sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15"
|
||||
],
|
||||
"markers": "python_version >= '2.6'",
|
||||
"version": "==5.5.0"
|
||||
},
|
||||
"pep8-naming": {
|
||||
|
@ -1095,6 +1510,7 @@
|
|||
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
|
||||
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"pydocstyle": {
|
||||
|
@ -1102,6 +1518,7 @@
|
|||
"sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325",
|
||||
"sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==5.1.1"
|
||||
},
|
||||
"pyflakes": {
|
||||
|
@ -1109,6 +1526,7 @@
|
|||
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
|
||||
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"pylint": {
|
||||
|
@ -1201,6 +1619,7 @@
|
|||
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.24.0"
|
||||
},
|
||||
"requirements-detector": {
|
||||
|
@ -1228,6 +1647,7 @@
|
|||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"smmap": {
|
||||
|
@ -1235,6 +1655,7 @@
|
|||
"sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4",
|
||||
"sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
|
@ -1249,6 +1670,7 @@
|
|||
"sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
|
||||
"sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.3.1"
|
||||
},
|
||||
"stevedore": {
|
||||
|
@ -1256,6 +1678,7 @@
|
|||
"sha256:a34086819e2c7a7f86d5635363632829dab8014e5fd7be2454c7cba84ac7514e",
|
||||
"sha256:ddc09a744dc224c84ec8e8efcb70595042d21c97c76df60daee64c9ad53bc7ee"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.2.1"
|
||||
},
|
||||
"toml": {
|
||||
|
@ -1308,7 +1731,6 @@
|
|||
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": null,
|
||||
"version": "==1.25.10"
|
||||
},
|
||||
"websocket-client": {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
![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)
|
||||
|
||||
|
|
|
@ -261,18 +261,6 @@ stages:
|
|||
command: 'buildAndPush'
|
||||
Dockerfile: 'Dockerfile'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
- job: build_gatekeeper
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: Docker@2
|
||||
inputs:
|
||||
containerRegistry: 'dockerhub'
|
||||
repository: 'beryju/passbook-gatekeeper'
|
||||
command: 'buildAndPush'
|
||||
Dockerfile: 'gatekeeper/Dockerfile'
|
||||
buildContext: 'gatekeeper/'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
- job: build_static
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
|
|
@ -22,14 +22,13 @@ services:
|
|||
- traefik.enable=false
|
||||
server:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-latest}
|
||||
command:
|
||||
- uwsgi
|
||||
- uwsgi.ini
|
||||
command: server
|
||||
environment:
|
||||
- PASSBOOK_REDIS__HOST=redis
|
||||
- PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false}
|
||||
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
||||
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
||||
PASSBOOK_REDIS__HOST: redis
|
||||
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
|
||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword}
|
||||
PASSBOOK_LOG_LEVEL: debug
|
||||
ports:
|
||||
- 8000
|
||||
networks:
|
||||
|
@ -40,23 +39,17 @@ services:
|
|||
- traefik.frontend.rule=PathPrefix:/
|
||||
worker:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-latest}
|
||||
command:
|
||||
- celery
|
||||
- worker
|
||||
- --autoscale=10,3
|
||||
- -E
|
||||
- -B
|
||||
- -A=passbook.root.celery
|
||||
- -s=/tmp/celerybeat-schedule
|
||||
command: worker
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
environment:
|
||||
- PASSBOOK_REDIS__HOST=redis
|
||||
- PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false}
|
||||
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
||||
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
||||
PASSBOOK_REDIS__HOST: redis
|
||||
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
|
||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword}
|
||||
PASSBOOK_LOG_LEVEL: debug
|
||||
static:
|
||||
image: beryju/passbook-static:latest
|
||||
networks:
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
#!/bin/bash -ex
|
||||
#!/bin/bash -e
|
||||
/app/wait_for_db.py
|
||||
"$@"
|
||||
printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@"
|
||||
if [[ "$1" == "server" ]]; then
|
||||
gunicorn -c gunicorn.conf.py passbook.root.asgi:application
|
||||
elif [[ "$1" == "worker" ]]; then
|
||||
celery worker --autoscale=10,3 -E -B -A=passbook.root.celery -s=/tmp/celerybeat-schedule
|
||||
else
|
||||
./manage.py "$@"
|
||||
fi
|
||||
|
|
38
docker/gunicorn.conf.py
Normal file
38
docker/gunicorn.conf.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
"""Gunicorn config"""
|
||||
import multiprocessing
|
||||
|
||||
import structlog
|
||||
|
||||
bind = "0.0.0.0:8000"
|
||||
workers = multiprocessing.cpu_count() * 2 + 1
|
||||
workers = 1
|
||||
|
||||
user = "passbook"
|
||||
group = "passbook"
|
||||
|
||||
worker_class = "uvicorn.workers.UvicornWorker"
|
||||
|
||||
logconfig_dict = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"json_formatter": {
|
||||
"()": structlog.stdlib.ProcessorFormatter,
|
||||
"processor": structlog.processors.JSONRenderer(),
|
||||
"foreign_pre_chain": [
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.processors.TimeStamper(),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.format_exc_info,
|
||||
],
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"error_console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "json_formatter",
|
||||
},
|
||||
"console": {"class": "logging.StreamHandler", "formatter": "json_formatter"},
|
||||
},
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
[uwsgi]
|
||||
http = 0.0.0.0:8000
|
||||
wsgi-file = passbook/root/wsgi.py
|
||||
processes = 2
|
||||
master = true
|
||||
threads = 2
|
||||
enable-threads = true
|
||||
uid = passbook
|
||||
gid = passbook
|
||||
disable-logging = True
|
|
@ -6,3 +6,4 @@ services:
|
|||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
network_mode: host
|
||||
restart: always
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
FROM quay.io/oauth2-proxy/oauth2-proxy
|
||||
|
||||
ENV OAUTH2_PROXY_EMAIL_DOMAINS=*
|
||||
ENV OAUTH2_PROXY_PROVIDER=oidc
|
||||
ENV OAUTH2_PROXY_HTTP_ADDRESS=:4180
|
||||
# TODO: If service is access over HTTPS, this needs to be set to true (default), otherwise needs to be false
|
||||
# ENV OAUTH2_PROXY_COOKIE_SECURE=true
|
||||
ENV OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true
|
|
@ -53,9 +53,7 @@ spec:
|
|||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- uwsgi
|
||||
- uwsgi.ini
|
||||
args: server
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
|
|
|
@ -26,14 +26,7 @@ spec:
|
|||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- celery
|
||||
- worker
|
||||
- --autoscale=10,3
|
||||
- -E
|
||||
- -B
|
||||
- -A=passbook.root.celery
|
||||
- -s=/tmp/celerybeat-schedule
|
||||
args: worker
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: "{{ include "passbook.fullname" . }}-config"
|
||||
|
|
|
@ -46,6 +46,12 @@
|
|||
{% trans 'Providers' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:outposts' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:outposts' 'passbook_admin:outpost-create' 'passbook_admin:outpost-update' 'passbook_admin:outpost-delete' %}">
|
||||
{% trans 'Outposts' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:property-mappings' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:property-mappings' 'passbook_admin:property-mapping-create' 'passbook_admin:property-mapping-update' 'passbook_admin:property-mapping-delete' %}">
|
||||
|
|
96
passbook/admin/templates/administration/outpost/list.html
Normal file
96
passbook/admin/templates/administration/outpost/list.html
Normal file
|
@ -0,0 +1,96 @@
|
|||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.pf-m-success {
|
||||
color: var(--pf-global--success-color--100);
|
||||
}
|
||||
.pf-m-danger {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="fas fa-map-marker"></i>
|
||||
{% trans 'Outposts' %}
|
||||
</h1>
|
||||
<p>{% trans "Outposts are deployments of passbook components to support different environments and protocols, like reverse proxies." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Providers' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Health' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for outpost in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<a href="{% url 'passbook_outposts:setup' outpost_pk=outpost.pk %}">{{ outpost.name }}</a>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ outpost.providers.all.select_subclasses|join:", " }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
{% with health=outpost.health %}
|
||||
{% if health %}
|
||||
<i class="fas fa-check pf-m-success"></i> {{ health|naturaltime }}
|
||||
{% else %}
|
||||
<i class="fas fa-times pf-m-danger"></i> Unhealthy
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td>
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:outpost-update' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:outpost-delete' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Outposts.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no outposts exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:outpost-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -36,7 +36,7 @@
|
|||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ token.pk }}</div>
|
||||
<div>{{ token.pk.hex }}</div>
|
||||
</div>
|
||||
</th>
|
||||
<td role="cell">
|
||||
|
@ -51,7 +51,11 @@
|
|||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{% if not token.expiring %}
|
||||
-
|
||||
{% else %}
|
||||
{{ token.expires }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
|
|
|
@ -6,6 +6,7 @@ from passbook.admin.views import (
|
|||
certificate_key_pair,
|
||||
flows,
|
||||
groups,
|
||||
outposts,
|
||||
overview,
|
||||
policies,
|
||||
policies_bindings,
|
||||
|
@ -271,4 +272,19 @@ urlpatterns = [
|
|||
certificate_key_pair.CertificateKeyPairDeleteView.as_view(),
|
||||
name="certificatekeypair-delete",
|
||||
),
|
||||
# Outposts
|
||||
path("outposts/", outposts.OutpostListView.as_view(), name="outposts",),
|
||||
path(
|
||||
"outposts/create/", outposts.OutpostCreateView.as_view(), name="outpost-create",
|
||||
),
|
||||
path(
|
||||
"outposts/<uuid:pk>/update/",
|
||||
outposts.OutpostUpdateView.as_view(),
|
||||
name="outpost-update",
|
||||
),
|
||||
path(
|
||||
"outposts/<uuid:pk>/delete/",
|
||||
outposts.OutpostDeleteView.as_view(),
|
||||
name="outpost-delete",
|
||||
),
|
||||
]
|
||||
|
|
67
passbook/admin/views/outposts.py
Normal file
67
passbook/admin/views/outposts.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
"""passbook Outpost administration"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.outposts.forms import OutpostForm
|
||||
from passbook.outposts.models import Outpost
|
||||
|
||||
|
||||
class OutpostListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all outposts"""
|
||||
|
||||
model = Outpost
|
||||
permission_required = "passbook_outposts.view_outpost"
|
||||
ordering = "name"
|
||||
paginate_by = 40
|
||||
template_name = "administration/outpost/list.html"
|
||||
|
||||
|
||||
class OutpostCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create new Outpost"""
|
||||
|
||||
model = Outpost
|
||||
form_class = OutpostForm
|
||||
permission_required = "passbook_outposts.add_outpost"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("passbook_admin:outposts")
|
||||
success_message = _("Successfully created Outpost")
|
||||
|
||||
|
||||
class OutpostUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update outpost"""
|
||||
|
||||
model = Outpost
|
||||
form_class = OutpostForm
|
||||
permission_required = "passbook_outposts.change_outpost"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:outposts")
|
||||
success_message = _("Successfully updated Certificate-Key Pair")
|
||||
|
||||
|
||||
class OutpostDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete outpost"""
|
||||
|
||||
model = Outpost
|
||||
permission_required = "passbook_outposts.delete_outpost"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:outposts")
|
||||
success_message = _("Successfully deleted Certificate-Key Pair")
|
|
@ -6,15 +6,17 @@ from drf_yasg.views import get_schema_view
|
|||
from rest_framework import routers
|
||||
|
||||
from passbook.api.permissions import CustomObjectPermissions
|
||||
from passbook.api.v2.messages import MessagesViewSet
|
||||
from passbook.audit.api import EventViewSet
|
||||
from passbook.core.api.applications import ApplicationViewSet
|
||||
from passbook.core.api.groups import GroupViewSet
|
||||
from passbook.core.api.messages import MessagesViewSet
|
||||
from passbook.core.api.propertymappings import PropertyMappingViewSet
|
||||
from passbook.core.api.providers import ProviderViewSet
|
||||
from passbook.core.api.sources import SourceViewSet
|
||||
from passbook.core.api.users import UserViewSet
|
||||
from passbook.crypto.api import CertificateKeyPairViewSet
|
||||
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
|
||||
from passbook.outposts.api import OutpostViewSet
|
||||
from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet
|
||||
from passbook.policies.dummy.api import DummyPolicyViewSet
|
||||
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
|
||||
|
@ -24,7 +26,7 @@ from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet
|
|||
from passbook.policies.password.api import PasswordPolicyViewSet
|
||||
from passbook.policies.reputation.api import ReputationPolicyViewSet
|
||||
from passbook.providers.oauth2.api import OAuth2ProviderViewSet, ScopeMappingViewSet
|
||||
from passbook.providers.proxy.api import ProxyProviderViewSet
|
||||
from passbook.providers.proxy.api import OutpostConfigViewSet, ProxyProviderViewSet
|
||||
from passbook.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet
|
||||
from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
||||
from passbook.sources.oauth.api import OAuthSourceViewSet
|
||||
|
@ -47,10 +49,14 @@ from passbook.stages.user_write.api import UserWriteStageViewSet
|
|||
|
||||
router = routers.DefaultRouter()
|
||||
|
||||
router.register("root/messages", MessagesViewSet, basename="messages")
|
||||
router.register("core/applications", ApplicationViewSet)
|
||||
router.register("core/groups", GroupViewSet)
|
||||
router.register("core/users", UserViewSet)
|
||||
router.register("core/messages", MessagesViewSet, basename="messages")
|
||||
router.register("outposts/outposts", OutpostViewSet)
|
||||
router.register("outposts/proxy", OutpostConfigViewSet)
|
||||
|
||||
router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
|
||||
|
||||
router.register("audit/events", EventViewSet)
|
||||
|
||||
|
|
|
@ -9,10 +9,11 @@ from passbook.lib.widgets import GroupedModelChoiceField
|
|||
class ApplicationForm(forms.ModelForm):
|
||||
"""Application Form"""
|
||||
|
||||
provider = GroupedModelChoiceField(
|
||||
queryset=Provider.objects.all().order_by("pk").select_subclasses(),
|
||||
required=False,
|
||||
)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["provider"].queryset = (
|
||||
Provider.objects.all().order_by("pk").select_subclasses()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
|
@ -32,6 +33,7 @@ class ApplicationForm(forms.ModelForm):
|
|||
"meta_icon_url": forms.TextInput(),
|
||||
"meta_publisher": forms.TextInput(),
|
||||
}
|
||||
field_classes = {"provider": GroupedModelChoiceField}
|
||||
labels = {
|
||||
"meta_launch_url": _("Launch URL"),
|
||||
"meta_icon_url": _("Icon URL"),
|
||||
|
|
36
passbook/core/migrations/0008_auto_20200824_1532.py
Normal file
36
passbook/core/migrations/0008_auto_20200824_1532.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 3.1 on 2020-08-24 15:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("passbook_core", "0007_auto_20200815_1841"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="groups",
|
||||
field=models.ManyToManyField(to="passbook_core.Group"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="groups",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="pb_groups",
|
||||
field=models.ManyToManyField(to="passbook_core.Group"),
|
||||
),
|
||||
]
|
|
@ -58,7 +58,7 @@ class User(GuardianUserMixin, AbstractUser):
|
|||
name = models.TextField(help_text=_("User's display name."))
|
||||
|
||||
sources = models.ManyToManyField("Source", through="UserSourceConnection")
|
||||
groups = models.ManyToManyField("Group")
|
||||
pb_groups = models.ManyToManyField("Group")
|
||||
password_change_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
attributes = models.JSONField(default=dict, blank=True)
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
{% trans card_title %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
{% trans card_title %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
{% if message %}
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<main class="pf-c-login__main">
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% block title %}
|
||||
{% block card_title %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
</header>
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
{% load i18n %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block card_title %}
|
||||
{% trans 'Permission denied' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'Permission denied' %}
|
||||
{% endblock %}
|
||||
|
|
47
passbook/crypto/api.py
Normal file
47
passbook/crypto/api.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
"""Crypto API Views"""
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
from rest_framework.serializers import ModelSerializer, ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
|
||||
|
||||
class CertificateKeyPairSerializer(ModelSerializer):
|
||||
"""CertificateKeyPair Serializer"""
|
||||
|
||||
def validate_certificate_data(self, value):
|
||||
"""Verify that input is a valid PEM x509 Certificate"""
|
||||
try:
|
||||
load_pem_x509_certificate(value.encode("utf-8"), default_backend())
|
||||
except ValueError:
|
||||
raise ValidationError("Unable to load certificate.")
|
||||
return value
|
||||
|
||||
def validate_key_data(self, value):
|
||||
"""Verify that input is a valid PEM RSA Key"""
|
||||
# Since this field is optional, data can be empty.
|
||||
if value == "":
|
||||
return value
|
||||
try:
|
||||
load_pem_private_key(
|
||||
str.encode("\n".join([x.strip() for x in value.split("\n")])),
|
||||
password=None,
|
||||
backend=default_backend(),
|
||||
)
|
||||
except ValueError:
|
||||
raise ValidationError("Unable to load private key.")
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
|
||||
model = CertificateKeyPair
|
||||
fields = ["pk", "name", "certificate_data", "key_data"]
|
||||
|
||||
|
||||
class CertificateKeyPairViewSet(ModelViewSet):
|
||||
"""CertificateKeyPair Viewset"""
|
||||
|
||||
queryset = CertificateKeyPair.objects.all()
|
||||
serializer_class = CertificateKeyPairSerializer
|
|
@ -13,5 +13,4 @@ class PassbookFlowsConfig(AppConfig):
|
|||
verbose_name = "passbook Flows"
|
||||
|
||||
def ready(self):
|
||||
"""Flow signals that clear the cache"""
|
||||
import_module("passbook.flows.signals")
|
||||
|
|
18
passbook/flows/migrations/0012_auto_20200830_1056.py
Normal file
18
passbook/flows/migrations/0012_auto_20200830_1056.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1 on 2020-08-30 10:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_flows", "0011_flow_title"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="flow",
|
||||
name="title",
|
||||
field=models.TextField(blank=True, default=""),
|
||||
),
|
||||
]
|
|
@ -3,18 +3,17 @@ import os
|
|||
from collections.abc import Mapping
|
||||
from contextlib import contextmanager
|
||||
from glob import glob
|
||||
from json import dumps
|
||||
from typing import Any, Dict
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import yaml
|
||||
from django.conf import ImproperlyConfigured
|
||||
from django.http import HttpRequest
|
||||
from structlog import get_logger
|
||||
|
||||
SEARCH_PATHS = ["passbook/lib/default.yml", "/etc/passbook/config.yml", ""] + glob(
|
||||
"/etc/passbook/config.d/*.yml", recursive=True
|
||||
)
|
||||
LOGGER = get_logger()
|
||||
ENV_PREFIX = "PASSBOOK"
|
||||
ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local")
|
||||
|
||||
|
@ -58,6 +57,13 @@ class ConfigLoader:
|
|||
self.update_from_file(env_file)
|
||||
self.update_from_env()
|
||||
|
||||
def _log(self, level: str, message: str, **kwargs):
|
||||
"""Custom Log method, we want to ensure ConfigLoader always logs JSON even when
|
||||
'structlog' or 'logging' hasn't been configured yet."""
|
||||
output = {"event": message, "level": level, "logger": self.__class__.__module__}
|
||||
output.update(kwargs)
|
||||
print(dumps(output))
|
||||
|
||||
def update(self, root, updatee):
|
||||
"""Recursively update dictionary"""
|
||||
for key, value in updatee.items():
|
||||
|
@ -82,12 +88,14 @@ class ConfigLoader:
|
|||
with open(path) as file:
|
||||
try:
|
||||
self.update(self.__config, yaml.safe_load(file))
|
||||
LOGGER.debug("Loaded config", file=path)
|
||||
self._log("debug", "Loaded config", file=path)
|
||||
self.loaded_file.append(path)
|
||||
except yaml.YAMLError as exc:
|
||||
raise ImproperlyConfigured from exc
|
||||
except PermissionError as exc:
|
||||
LOGGER.warning("Permission denied while reading file", path=path, error=exc)
|
||||
self._log(
|
||||
"warning", "Permission denied while reading file", path=path, error=exc
|
||||
)
|
||||
|
||||
def update_from_dict(self, update: dict):
|
||||
"""Update config from dict"""
|
||||
|
@ -111,7 +119,7 @@ class ConfigLoader:
|
|||
current_obj[dot_parts[-1]] = value
|
||||
idx += 1
|
||||
if idx > 0:
|
||||
LOGGER.debug("Loaded environment variables", count=idx)
|
||||
self._log("debug", "Loaded environment variables", count=idx)
|
||||
self.update(self.__config, outer)
|
||||
|
||||
@contextmanager
|
||||
|
|
|
@ -12,7 +12,7 @@ redis:
|
|||
message_queue_db: 1
|
||||
|
||||
debug: false
|
||||
log_level: warning
|
||||
log_level: info
|
||||
|
||||
# Error reporting, sends stacktrace to sentry.beryju.org
|
||||
error_reporting:
|
||||
|
|
0
passbook/outposts/__init__.py
Normal file
0
passbook/outposts/__init__.py
Normal file
23
passbook/outposts/api.py
Normal file
23
passbook/outposts/api.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
"""Outpost API Views"""
|
||||
from rest_framework.serializers import JSONField, ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from passbook.outposts.models import Outpost
|
||||
|
||||
|
||||
class OutpostSerializer(ModelSerializer):
|
||||
"""Outpost Serializer"""
|
||||
|
||||
_config = JSONField()
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Outpost
|
||||
fields = ["pk", "name", "providers", "_config"]
|
||||
|
||||
|
||||
class OutpostViewSet(ModelViewSet):
|
||||
"""Outpost Viewset"""
|
||||
|
||||
queryset = Outpost.objects.all()
|
||||
serializer_class = OutpostSerializer
|
16
passbook/outposts/apps.py
Normal file
16
passbook/outposts/apps.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""passbook outposts app config"""
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PassbookOutpostConfig(AppConfig):
|
||||
"""passbook outposts app config"""
|
||||
|
||||
name = "passbook.outposts"
|
||||
label = "passbook_outposts"
|
||||
mountpoint = "outposts/"
|
||||
verbose_name = "passbook Outpost"
|
||||
|
||||
def ready(self):
|
||||
import_module("passbook.outposts.signals")
|
100
passbook/outposts/channels.py
Normal file
100
passbook/outposts/channels.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
"""Outpost websocket handler"""
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from enum import IntEnum
|
||||
from time import time
|
||||
from typing import Any, Dict
|
||||
|
||||
from channels.generic.websocket import JsonWebsocketConsumer
|
||||
from dacite import from_dict
|
||||
from dacite.data import Data
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Token, TokenIntents
|
||||
from passbook.outposts.models import Outpost
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class WebsocketMessageInstruction(IntEnum):
|
||||
"""Commands which can be triggered over Websocket"""
|
||||
|
||||
# Simple message used by either side when a message is acknowledged
|
||||
ACK = 0
|
||||
|
||||
# Message used by outposts to report their alive status
|
||||
HELLO = 1
|
||||
|
||||
# Message sent by us to trigger an Update
|
||||
TRIGGER_UPDATE = 2
|
||||
|
||||
|
||||
@dataclass
|
||||
class WebsocketMessage:
|
||||
"""Complete Websocket Message that is being sent"""
|
||||
|
||||
instruction: int
|
||||
args: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class OutpostConsumer(JsonWebsocketConsumer):
|
||||
"""Handler for Outposts that connect over websockets for health checks and live updates"""
|
||||
|
||||
outpost: Outpost
|
||||
|
||||
def connect(self):
|
||||
# TODO: This authentication block could be handeled in middleware
|
||||
headers = dict(self.scope["headers"])
|
||||
if b"authorization" not in headers:
|
||||
LOGGER.warning("WS Request without authorization header")
|
||||
self.close()
|
||||
|
||||
token = headers[b"authorization"]
|
||||
try:
|
||||
token_uuid = token.decode("utf-8")
|
||||
tokens = Token.filter_not_expired(
|
||||
token_uuid=token_uuid, intent=TokenIntents.INTENT_API
|
||||
)
|
||||
if not tokens.exists():
|
||||
LOGGER.warning("WS Request with invalid token")
|
||||
self.close()
|
||||
except ValidationError:
|
||||
LOGGER.warning("WS Invalid UUID")
|
||||
self.close()
|
||||
|
||||
uuid = self.scope["url_route"]["kwargs"]["pk"]
|
||||
outpost = Outpost.objects.filter(pk=uuid)
|
||||
if not outpost.exists():
|
||||
self.close()
|
||||
return
|
||||
self.accept()
|
||||
self.outpost = outpost.first()
|
||||
self.outpost.channels.append(self.channel_name)
|
||||
LOGGER.debug("added channel to outpost", channel_name=self.channel_name)
|
||||
self.outpost.save()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def disconnect(self, close_code):
|
||||
self.outpost.channels.remove(self.channel_name)
|
||||
self.outpost.save()
|
||||
LOGGER.debug("removed channel from outpost", channel_name=self.channel_name)
|
||||
|
||||
def receive_json(self, content: Data):
|
||||
msg = from_dict(WebsocketMessage, content)
|
||||
if msg.instruction == WebsocketMessageInstruction.HELLO:
|
||||
cache.set(self.outpost.health_cache_key, time(), timeout=60)
|
||||
elif msg.instruction == WebsocketMessageInstruction.ACK:
|
||||
return
|
||||
|
||||
response = WebsocketMessage(instruction=WebsocketMessageInstruction.ACK)
|
||||
self.send_json(asdict(response))
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def event_update(self, event):
|
||||
"""Event handler which is called by post_save signals"""
|
||||
self.send_json(
|
||||
asdict(
|
||||
WebsocketMessage(instruction=WebsocketMessageInstruction.TRIGGER_UPDATE)
|
||||
)
|
||||
)
|
0
passbook/outposts/controllers/__init__.py
Normal file
0
passbook/outposts/controllers/__init__.py
Normal file
29
passbook/outposts/controllers/base.py
Normal file
29
passbook/outposts/controllers/base.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
"""Base Controller"""
|
||||
from typing import Dict
|
||||
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.outposts.models import Outpost
|
||||
|
||||
|
||||
class BaseController:
|
||||
"""Base Outpost deployment controller"""
|
||||
|
||||
deployment_ports: Dict[str, int]
|
||||
|
||||
outpost: Outpost
|
||||
|
||||
def __init__(self, outpost_pk: str):
|
||||
self.outpost = Outpost.objects.get(pk=outpost_pk)
|
||||
self.logger = get_logger(
|
||||
controller=self.__class__.__name__, outpost=self.outpost
|
||||
)
|
||||
self.deployment_ports = {}
|
||||
|
||||
def run(self):
|
||||
"""Called by scheduled task to reconcile deployment/service/etc"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_static_deployment(self) -> str:
|
||||
"""Return a static deployment configuration"""
|
||||
raise NotImplementedError
|
36
passbook/outposts/controllers/compose.py
Normal file
36
passbook/outposts/controllers/compose.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
"""Docker Compose controller"""
|
||||
from yaml import safe_dump
|
||||
|
||||
from passbook import __version__
|
||||
from passbook.outposts.controllers.base import BaseController
|
||||
|
||||
|
||||
class DockerComposeController(BaseController):
|
||||
"""Docker Compose controller"""
|
||||
|
||||
image_base = "beryju/passbook"
|
||||
|
||||
def run(self):
|
||||
self.logger.warning("DockerComposeController does not implement run")
|
||||
raise NotImplementedError
|
||||
|
||||
def get_static_deployment(self) -> str:
|
||||
"""Generate docker-compose yaml for proxy, version 3.5"""
|
||||
ports = [f"{x}:{x}" for _, x in self.deployment_ports.items()]
|
||||
compose = {
|
||||
"version": "3.5",
|
||||
"services": {
|
||||
f"passbook_{self.outpost.type}": {
|
||||
"image": f"{self.image_base}-{self.outpost.type}:{__version__}",
|
||||
"ports": ports,
|
||||
"environment": {
|
||||
"PASSBOOK_HOST": self.outpost.config.passbook_host,
|
||||
"PASSBOOK_INSECURE": str(
|
||||
self.outpost.config.passbook_host_insecure
|
||||
),
|
||||
"PASSBOOK_TOKEN": self.outpost.token.token_uuid.hex,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
return safe_dump(compose, default_flow_style=False)
|
143
passbook/outposts/controllers/kubernetes.py
Normal file
143
passbook/outposts/controllers/kubernetes.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
"""Kubernetes deployment controller"""
|
||||
from io import StringIO
|
||||
|
||||
from kubernetes.client import (
|
||||
V1Container,
|
||||
V1ContainerPort,
|
||||
V1Deployment,
|
||||
V1DeploymentSpec,
|
||||
V1EnvVar,
|
||||
V1EnvVarSource,
|
||||
V1LabelSelector,
|
||||
V1ObjectMeta,
|
||||
V1PodSpec,
|
||||
V1PodTemplateSpec,
|
||||
V1Secret,
|
||||
V1SecretKeySelector,
|
||||
V1Service,
|
||||
V1ServicePort,
|
||||
V1ServiceSpec,
|
||||
)
|
||||
from yaml import dump_all
|
||||
|
||||
from passbook import __version__
|
||||
from passbook.outposts.controllers.base import BaseController
|
||||
|
||||
|
||||
class KubernetesController(BaseController):
|
||||
"""Manage deployment of outpost in kubernetes"""
|
||||
|
||||
image_base = "beryju/passbook"
|
||||
|
||||
def run(self):
|
||||
"""Called by scheduled task to reconcile deployment/service/etc"""
|
||||
# TODO
|
||||
|
||||
def get_static_deployment(self) -> str:
|
||||
with StringIO() as _str:
|
||||
dump_all(
|
||||
[
|
||||
self.get_deployment_secret(),
|
||||
self.get_deployment(),
|
||||
self.get_service(),
|
||||
],
|
||||
stream=_str,
|
||||
default_flow_style=False,
|
||||
)
|
||||
return _str.getvalue()
|
||||
|
||||
def get_object_meta(self, **kwargs) -> V1ObjectMeta:
|
||||
"""Get common object metadata"""
|
||||
return V1ObjectMeta(
|
||||
namespace="self.instance.namespace",
|
||||
labels={
|
||||
"app.kubernetes.io/name": f"passbook-{self.outpost.type.lower()}",
|
||||
"app.kubernetes.io/instance": self.outpost.name,
|
||||
"app.kubernetes.io/version": __version__,
|
||||
"app.kubernetes.io/managed-by": "passbook.beryju.org",
|
||||
"passbook.beryju.org/outpost/uuid": self.outpost.uuid.hex,
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def get_deployment_secret(self) -> V1Secret:
|
||||
"""Get secret with token and passbook host"""
|
||||
return V1Secret(
|
||||
metadata=self.get_object_meta(
|
||||
name=f"passbook-outpost-{self.outpost.name}-api"
|
||||
),
|
||||
data={
|
||||
"passbook_host": self.outpost.config.passbook_host,
|
||||
"passbook_host_insecure": str(
|
||||
self.outpost.config.passbook_host_insecure
|
||||
),
|
||||
"token": self.outpost.token.token_uuid.hex,
|
||||
},
|
||||
)
|
||||
|
||||
def get_service(self) -> V1Service:
|
||||
"""Get service object for outpost based on ports defined"""
|
||||
meta = self.get_object_meta(name=f"passbook-outpost-{self.outpost.name}")
|
||||
ports = []
|
||||
for port_name, port in self.deployment_ports.items():
|
||||
ports.append(V1ServicePort(name=port_name, port=port))
|
||||
return V1Service(
|
||||
metadata=meta,
|
||||
spec=V1ServiceSpec(ports=ports, selector=meta.labels, type="ClusterIP"),
|
||||
)
|
||||
|
||||
def get_deployment(self) -> V1Deployment:
|
||||
"""Get deployment object for outpost"""
|
||||
# Generate V1ContainerPort objects
|
||||
container_ports = []
|
||||
for port_name, port in self.deployment_ports.items():
|
||||
container_ports.append(V1ContainerPort(container_port=port, name=port_name))
|
||||
meta = self.get_object_meta(name=f"passbook-outpost-{self.outpost.name}")
|
||||
return V1Deployment(
|
||||
metadata=meta,
|
||||
spec=V1DeploymentSpec(
|
||||
replicas=1,
|
||||
selector=V1LabelSelector(match_labels=meta.labels),
|
||||
template=V1PodTemplateSpec(
|
||||
metadata=V1ObjectMeta(labels=meta.labels),
|
||||
spec=V1PodSpec(
|
||||
containers=[
|
||||
V1Container(
|
||||
name=self.outpost.type,
|
||||
image=f"{self.image_base}-{self.outpost.type}:{__version__}",
|
||||
ports=container_ports,
|
||||
env=[
|
||||
V1EnvVar(
|
||||
name="PASSBOOK_HOST",
|
||||
value_from=V1EnvVarSource(
|
||||
secret_key_ref=V1SecretKeySelector(
|
||||
name=f"passbook-outpost-{self.outpost.name}-api",
|
||||
key="passbook_host",
|
||||
)
|
||||
),
|
||||
),
|
||||
V1EnvVar(
|
||||
name="PASSBOOK_TOKEN",
|
||||
value_from=V1EnvVarSource(
|
||||
secret_key_ref=V1SecretKeySelector(
|
||||
name=f"passbook-outpost-{self.outpost.name}-api",
|
||||
key="token",
|
||||
)
|
||||
),
|
||||
),
|
||||
V1EnvVar(
|
||||
name="PASSBOOK_INSECURE",
|
||||
value_from=V1EnvVarSource(
|
||||
secret_key_ref=V1SecretKeySelector(
|
||||
name=f"passbook-outpost-{self.outpost.name}-api",
|
||||
key="passbook_host_insecure",
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
35
passbook/outposts/forms.py
Normal file
35
passbook/outposts/forms.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
"""Outpost forms"""
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.admin.fields import CodeMirrorWidget, YAMLField
|
||||
from passbook.core.models import Provider
|
||||
from passbook.outposts.models import Outpost
|
||||
|
||||
|
||||
class OutpostForm(forms.ModelForm):
|
||||
"""Outpost Form"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["providers"].queryset = Provider.objects.all().select_subclasses()
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Outpost
|
||||
fields = [
|
||||
"name",
|
||||
"type",
|
||||
"deployment_type",
|
||||
"providers",
|
||||
"_config",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"_config": CodeMirrorWidget,
|
||||
}
|
||||
field_classes = {
|
||||
"_config": YAMLField,
|
||||
}
|
||||
labels = {"_config": _("Configuration")}
|
40
passbook/outposts/migrations/0001_initial.py
Normal file
40
passbook/outposts/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 3.1 on 2020-08-25 20:45
|
||||
|
||||
import uuid
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0008_auto_20200824_1532"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Outpost",
|
||||
fields=[
|
||||
(
|
||||
"uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("name", models.TextField()),
|
||||
(
|
||||
"channels",
|
||||
django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(), size=None
|
||||
),
|
||||
),
|
||||
("providers", models.ManyToManyField(to="passbook_core.Provider")),
|
||||
],
|
||||
),
|
||||
]
|
27
passbook/outposts/migrations/0002_auto_20200826_1306.py
Normal file
27
passbook/outposts/migrations/0002_auto_20200826_1306.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 3.1 on 2020-08-26 13:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import passbook.outposts.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_outposts", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="outpost",
|
||||
name="_config",
|
||||
field=models.JSONField(
|
||||
default=passbook.outposts.models.default_outpost_config
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="outpost",
|
||||
name="type",
|
||||
field=models.TextField(choices=[("proxy", "Proxy")], default="proxy"),
|
||||
),
|
||||
]
|
34
passbook/outposts/migrations/0003_auto_20200827_2108.py
Normal file
34
passbook/outposts/migrations/0003_auto_20200827_2108.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Generated by Django 3.1 on 2020-08-27 21:08
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_outposts", "0002_auto_20200826_1306"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="outpost",
|
||||
name="deployment_type",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("docker_compose", "Docker Compose"),
|
||||
("kubernetes", "Kubernetes"),
|
||||
("custom", "Custom"),
|
||||
],
|
||||
default="custom",
|
||||
help_text="Select between passbook-managed deployment types or a custom deployment.",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="outpost",
|
||||
name="channels",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(), default=list, size=None
|
||||
),
|
||||
),
|
||||
]
|
22
passbook/outposts/migrations/0004_auto_20200830_1056.py
Normal file
22
passbook/outposts/migrations/0004_auto_20200830_1056.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 3.1 on 2020-08-30 10:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_outposts", "0003_auto_20200827_2108"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="outpost",
|
||||
name="deployment_type",
|
||||
field=models.TextField(
|
||||
choices=[("kubernetes", "Kubernetes"), ("custom", "Custom")],
|
||||
default="custom",
|
||||
help_text="Select between passbook-managed deployment types or a custom deployment.",
|
||||
),
|
||||
),
|
||||
]
|
0
passbook/outposts/migrations/__init__.py
Normal file
0
passbook/outposts/migrations/__init__.py
Normal file
148
passbook/outposts/models.py
Normal file
148
passbook/outposts/models.py
Normal file
|
@ -0,0 +1,148 @@
|
|||
"""Outpost models"""
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime
|
||||
from json import dumps, loads
|
||||
from typing import Iterable, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from dacite import from_dict
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from guardian.shortcuts import assign_perm
|
||||
|
||||
from passbook.core.models import Provider, Token, TokenIntents, User
|
||||
from passbook.lib.config import CONFIG
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutpostConfig:
|
||||
"""Configuration an outpost uses to configure it self"""
|
||||
|
||||
passbook_host: str
|
||||
passbook_host_insecure: bool = False
|
||||
|
||||
log_level: str = CONFIG.y("log_level")
|
||||
error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled")
|
||||
error_reporting_environment: str = CONFIG.y(
|
||||
"error_reporting.environment", "customer"
|
||||
)
|
||||
|
||||
|
||||
class OutpostModel:
|
||||
"""Base model for providers that need more objects than just themselves"""
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model]:
|
||||
"""Return a list of all required objects"""
|
||||
return [self]
|
||||
|
||||
|
||||
class OutpostType(models.TextChoices):
|
||||
"""Outpost types, currently only the reverse proxy is available"""
|
||||
|
||||
PROXY = "proxy"
|
||||
|
||||
|
||||
class OutpostDeploymentType(models.TextChoices):
|
||||
"""Deployment types that are managed through passbook"""
|
||||
|
||||
KUBERNETES = "kubernetes"
|
||||
CUSTOM = "custom"
|
||||
|
||||
|
||||
def default_outpost_config():
|
||||
"""Get default outpost config"""
|
||||
return asdict(OutpostConfig(passbook_host=""))
|
||||
|
||||
|
||||
class Outpost(models.Model):
|
||||
"""Outpost instance which manages a service user and token"""
|
||||
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
||||
name = models.TextField()
|
||||
|
||||
type = models.TextField(choices=OutpostType.choices, default=OutpostType.PROXY)
|
||||
deployment_type = models.TextField(
|
||||
choices=OutpostDeploymentType.choices,
|
||||
default=OutpostDeploymentType.CUSTOM,
|
||||
help_text=_(
|
||||
"Select between passbook-managed deployment types or a custom deployment."
|
||||
),
|
||||
)
|
||||
_config = models.JSONField(default=default_outpost_config)
|
||||
|
||||
providers = models.ManyToManyField(Provider)
|
||||
|
||||
channels = ArrayField(models.TextField(), default=list)
|
||||
|
||||
@property
|
||||
def config(self) -> OutpostConfig:
|
||||
"""Load config as OutpostConfig object"""
|
||||
return from_dict(OutpostConfig, loads(self._config))
|
||||
|
||||
@config.setter
|
||||
def config(self, value):
|
||||
"""Dump config into json"""
|
||||
self._config = dumps(asdict(value))
|
||||
|
||||
@property
|
||||
def health_cache_key(self) -> str:
|
||||
"""Key by which the outposts health status is saved"""
|
||||
return f"outpost_{self.uuid.hex}_health"
|
||||
|
||||
@property
|
||||
def health(self) -> Optional[datetime]:
|
||||
"""Get outpost's health status"""
|
||||
key = self.health_cache_key
|
||||
value = cache.get(key, None)
|
||||
if value:
|
||||
return datetime.fromtimestamp(value)
|
||||
return None
|
||||
|
||||
def _create_user(self) -> User:
|
||||
"""Create user and assign permissions for all required objects"""
|
||||
user: User = User.objects.create(username=f"pb-outpost-{self.uuid.hex}")
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
for model in self.get_required_objects():
|
||||
assign_perm(
|
||||
f"{model._meta.app_label}.view_{model._meta.model_name}", user, model
|
||||
)
|
||||
return user
|
||||
|
||||
@property
|
||||
def user(self) -> User:
|
||||
"""Get/create user with access to all required objects"""
|
||||
user = User.objects.filter(username=f"pb-outpost-{self.uuid.hex}")
|
||||
if user.exists():
|
||||
return user.first()
|
||||
return self._create_user()
|
||||
|
||||
@property
|
||||
def token(self) -> Token:
|
||||
"""Get/create token for auto-generated user"""
|
||||
token = Token.filter_not_expired(user=self.user, intent=TokenIntents.INTENT_API)
|
||||
if token.exists():
|
||||
return token.first()
|
||||
return Token.objects.create(
|
||||
user=self.user,
|
||||
intent=TokenIntents.INTENT_API,
|
||||
description=f"Autogenerated by passbook for Outpost {self.name}",
|
||||
expiring=False,
|
||||
)
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model]:
|
||||
"""Get an iterator of all objects the user needs read access to"""
|
||||
objects = [self]
|
||||
for provider in (
|
||||
Provider.objects.filter(outpost=self).select_related().select_subclasses()
|
||||
):
|
||||
if isinstance(provider, OutpostModel):
|
||||
objects.extend(provider.get_required_objects())
|
||||
else:
|
||||
objects.append(provider)
|
||||
return objects
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Outpost {self.name}"
|
10
passbook/outposts/settings.py
Normal file
10
passbook/outposts/settings.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
"""Outposts Settings"""
|
||||
from celery.schedules import crontab
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"outposts_k8s": {
|
||||
"task": "passbook.outposts.tasks.outpost_k8s_controller",
|
||||
"schedule": crontab(minute="*/5"), # Run every 5 minutes
|
||||
"options": {"queue": "passbook_scheduled"},
|
||||
}
|
||||
}
|
58
passbook/outposts/signals.py
Normal file
58
passbook/outposts/signals.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
"""passbook outpost signals"""
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.outposts.models import Outpost, OutpostModel
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Outpost)
|
||||
# pylint: disable=unused-argument
|
||||
def ensure_user_and_token(sender, instance, **_):
|
||||
"""Ensure that token is created/updated on save"""
|
||||
_ = instance.token
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
# pylint: disable=unused-argument
|
||||
def post_save_update(sender, instance, **_):
|
||||
"""If an OutpostModel, or a model that is somehow connected to an OutpostModel is saved,
|
||||
we send a message down the relevant OutpostModels WS connection to trigger an update"""
|
||||
if isinstance(instance, OutpostModel):
|
||||
LOGGER.debug("triggering outpost update from outpostmodel", instance=instance)
|
||||
_send_update(instance)
|
||||
return
|
||||
|
||||
for field in instance._meta.get_fields():
|
||||
# Each field is checked if it has a `related_model` attribute (when ForeginKeys or M2Ms)
|
||||
# are used, and if it has a value
|
||||
if not hasattr(field, "related_model"):
|
||||
continue
|
||||
if not field.related_model:
|
||||
continue
|
||||
if not issubclass(field.related_model, OutpostModel):
|
||||
continue
|
||||
|
||||
field_name = f"{field.name}_set"
|
||||
if not hasattr(instance, field_name):
|
||||
continue
|
||||
|
||||
LOGGER.debug("triggering outpost update from from field", field=field.name)
|
||||
# Because the Outpost Model has an M2M to Provider,
|
||||
# we have to iterate over the entire QS
|
||||
for reverse in getattr(instance, field_name).all():
|
||||
_send_update(reverse)
|
||||
|
||||
|
||||
def _send_update(outpost_model: Model):
|
||||
"""Send update trigger for each channel of an outpost model"""
|
||||
for outpost in outpost_model.outpost_set.all():
|
||||
channel_layer = get_channel_layer()
|
||||
for channel in outpost.channels:
|
||||
print(f"sending update to channel {channel}")
|
||||
async_to_sync(channel_layer.send)(channel, {"type": "event.update"})
|
22
passbook/outposts/tasks.py
Normal file
22
passbook/outposts/tasks.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
"""outpost tasks"""
|
||||
from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType
|
||||
from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
||||
from passbook.root.celery import CELERY_APP
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True)
|
||||
# pylint: disable=unused-argument
|
||||
def outpost_k8s_controller(self):
|
||||
"""Launch Kubernetes Controller for all Outposts which are deployed in kubernetes"""
|
||||
for outpost in Outpost.objects.filter(
|
||||
deployment_type=OutpostDeploymentType.KUBERNETES
|
||||
):
|
||||
outpost_k8s_controller_single.delay(outpost.pk.hex, outpost.type)
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True)
|
||||
# pylint: disable=unused-argument
|
||||
def outpost_k8s_controller_single(self, outpost: str, outpost_type: str):
|
||||
"""Launch Kubernetes manager and reconcile deployment/service/etc"""
|
||||
if outpost_type == OutpostType.PROXY:
|
||||
ProxyKubernetesController(outpost).run()
|
|
@ -45,8 +45,8 @@
|
|||
<h1 class="pf-c-title pf-m-2xl">{% trans 'Setup with Kubernetes' %}</h1>
|
||||
</div>
|
||||
<div class="pf-c-modal-box__body">
|
||||
<p>{% trans 'Download the manifest to create the Gatekeeper deployment and service:' %}</p>
|
||||
<a href="{% url 'passbook_providers_app_gw:k8s-manifest' provider=provider.pk %}">{% trans 'Here' %}</a>
|
||||
<p>{% trans 'Download the manifest to create the Proxy deployment and service:' %}</p>
|
||||
<a href="{% url 'passbook_providers_proxy:k8s-manifest' provider=provider.pk %}">{% trans 'Here' %}</a>
|
||||
<p>{% trans 'Afterwards, add the following annotations to the Ingress you want to secure:' %}</p>
|
||||
<textarea class="codemirror" readonly data-cm-mode="yaml">
|
||||
nginx.ingress.kubernetes.io/auth-signin: https://$host/oauth2/start?rd=$escaped_request_uri
|
59
passbook/outposts/templates/outposts/setup_dc.html
Normal file
59
passbook/outposts/templates/outposts/setup_dc.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.pf-m-success {
|
||||
color: var(--pf-global--success-color--100);
|
||||
}
|
||||
.pf-m-danger {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="fas fa-map-marker"></i>
|
||||
{% trans 'Outpost Setup' %}
|
||||
</h1>
|
||||
<p>{% trans "Outposts are deployments of passbook components to support different environments and protocols, like reverse proxies." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<div class="pf-c-tabs pf-m-fill" id="filled-example">
|
||||
<button class="pf-c-tabs__scroll-button" disabled aria-hidden="true" aria-label="Scroll left">
|
||||
<i class="fas fa-angle-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-tabs__list">
|
||||
<li class="pf-c-tabs__item">
|
||||
<button class="pf-c-tabs__link" id="filled-example-users-link">
|
||||
<span class="pf-c-tabs__item-text">Users</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="pf-c-tabs__item pf-m-current">
|
||||
<button class="pf-c-tabs__link" id="filled-example-containers-link">
|
||||
<span class="pf-c-tabs__item-text">Containers</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="pf-c-tabs__item">
|
||||
<button class="pf-c-tabs__link" id="filled-example-database-link">
|
||||
<span class="pf-c-tabs__item-text">Database</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<button class="pf-c-tabs__scroll-button" disabled aria-hidden="true" aria-label="Scroll right">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,59 @@
|
|||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.pf-m-success {
|
||||
color: var(--pf-global--success-color--100);
|
||||
}
|
||||
.pf-m-danger {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="fas fa-map-marker"></i>
|
||||
{% trans 'Outpost Setup' %}
|
||||
</h1>
|
||||
<p>{% trans "Outposts are deployments of passbook components to support different environments and protocols, like reverse proxies." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<div class="pf-c-tabs pf-m-fill" id="filled-example">
|
||||
<button class="pf-c-tabs__scroll-button" disabled aria-hidden="true" aria-label="Scroll left">
|
||||
<i class="fas fa-angle-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-tabs__list">
|
||||
<li class="pf-c-tabs__item">
|
||||
<button class="pf-c-tabs__link" id="filled-example-users-link">
|
||||
<span class="pf-c-tabs__item-text">Users</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="pf-c-tabs__item pf-m-current">
|
||||
<button class="pf-c-tabs__link" id="filled-example-containers-link">
|
||||
<span class="pf-c-tabs__item-text">Containers</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="pf-c-tabs__item">
|
||||
<button class="pf-c-tabs__link" id="filled-example-database-link">
|
||||
<span class="pf-c-tabs__item-text">Database</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<button class="pf-c-tabs__scroll-button" disabled aria-hidden="true" aria-label="Scroll right">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
96
passbook/outposts/templates/outposts/setup_k8s_manual.html
Normal file
96
passbook/outposts/templates/outposts/setup_k8s_manual.html
Normal file
|
@ -0,0 +1,96 @@
|
|||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="fas fa-map-marker"></i>
|
||||
{% trans 'Outpost Setup' %}
|
||||
</h1>
|
||||
<p>{% trans "Outposts are deployments of passbook components to support different environments and protocols, like reverse proxies." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
<pre>apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: "passbook-{{ outpost.type }}"
|
||||
app.kubernetes.io/instance: "{{ outpost.name }}"
|
||||
passbook.beryju.org/outpost: "{{ outpost.pk.hex }}"
|
||||
name: "passbook-{{ outpost.type }}-{{ outpost.name }}"
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: "passbook-{{ outpost.type }}"
|
||||
app.kubernetes.io/instance: "{{ outpost.name }}"
|
||||
passbook.beryju.org/outpost: "{{ outpost.pk.hex }}"
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: "passbook-{{ outpost.type }}"
|
||||
app.kubernetes.io/instance: "{{ outpost.name }}"
|
||||
passbook.beryju.org/outpost: "{{ outpost.pk.hex }}"
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: PASSBOOK_HOST
|
||||
value: "{{ host }}"
|
||||
- name: PASSBOOK_TOKEN
|
||||
value: "{{ outpost.token.pk.hex }}"
|
||||
image: beryju/passbook-{{ outpost.type }}:{{ version }}
|
||||
name: "passbook-{{ outpost.type }}"
|
||||
ports:
|
||||
- containerPort: 4180
|
||||
protocol: TCP
|
||||
name: http
|
||||
- containerPort: 4443
|
||||
protocol: TCP
|
||||
name: https
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: "passbook-{{ outpost.type }}"
|
||||
app.kubernetes.io/instance: "{{ outpost.name }}"
|
||||
passbook.beryju.org/outpost: "{{ outpost.pk.hex }}"
|
||||
name: "passbook-{{ outpost.type }}-{{ outpost.name }}"
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 4180
|
||||
protocol: TCP
|
||||
targetPort: 4180
|
||||
- name: https
|
||||
port: 4443
|
||||
protocol: TCP
|
||||
targetPort: 4443
|
||||
selector:
|
||||
app.kubernetes.io/name: "passbook-{{ outpost.type }}"
|
||||
app.kubernetes.io/instance: "{{ outpost.name }}"
|
||||
passbook.beryju.org/outpost: "{{ outpost.pk.hex }}"
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: "passbook-{{ outpost.type }}-{{ outpost.name }}"
|
||||
spec:
|
||||
rules:
|
||||
- host: "{{ provider.external_host }}"
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: "passbook-{{ outpost.type }}-{{ outpost.name }}"
|
||||
servicePort: 4180
|
||||
path: "/pbprox"
|
||||
</pre>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
11
passbook/outposts/urls.py
Normal file
11
passbook/outposts/urls.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
"""passbook outposts urls"""
|
||||
from django.urls import path
|
||||
|
||||
from passbook.outposts.views import KubernetesManifestView, SetupView
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"<uuid:outpost_pk>/k8s/", KubernetesManifestView.as_view(), name="k8s-manifest"
|
||||
),
|
||||
path("<uuid:outpost_pk>/", SetupView.as_view(), name="setup"),
|
||||
]
|
78
passbook/outposts/views.py
Normal file
78
passbook/outposts/views.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
"""passbook outpost views"""
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Model
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views import View
|
||||
from django.views.generic import TemplateView
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.outposts.controllers.compose import DockerComposeController
|
||||
from passbook.outposts.models import Outpost, OutpostType
|
||||
from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def get_object_for_user_or_404(user: User, perm: str, **filters) -> Model:
|
||||
"""Wrapper that combines get_objects_for_user and get_object_or_404"""
|
||||
return get_object_or_404(get_objects_for_user(user, perm), **filters)
|
||||
|
||||
|
||||
class DockerComposeView(LoginRequiredMixin, View):
|
||||
"""Generate docker-compose yaml"""
|
||||
|
||||
def get(self, request: HttpRequest, outpost_pk: str) -> HttpResponse:
|
||||
"""Render docker-compose file"""
|
||||
outpost: Outpost = get_object_for_user_or_404(
|
||||
request.user, "passbook_outposts.view_outpost", pk=outpost_pk,
|
||||
)
|
||||
manifest = ""
|
||||
if outpost.type == OutpostType.PROXY:
|
||||
controller = DockerComposeController(outpost_pk)
|
||||
manifest = controller.get_static_deployment()
|
||||
|
||||
return HttpResponse(manifest, content_type="text/vnd.yaml")
|
||||
|
||||
|
||||
class KubernetesManifestView(LoginRequiredMixin, View):
|
||||
"""Generate Kubernetes Deployment and SVC for proxy"""
|
||||
|
||||
def get(self, request: HttpRequest, outpost_pk: str) -> HttpResponse:
|
||||
"""Render deployment template"""
|
||||
outpost: Outpost = get_object_for_user_or_404(
|
||||
request.user, "passbook_outposts.view_outpost", pk=outpost_pk,
|
||||
)
|
||||
manifest = ""
|
||||
if outpost.type == OutpostType.PROXY:
|
||||
controller = ProxyKubernetesController(outpost_pk)
|
||||
manifest = controller.get_static_deployment()
|
||||
|
||||
return HttpResponse(manifest, content_type="text/vnd.yaml")
|
||||
|
||||
|
||||
class SetupView(LoginRequiredMixin, TemplateView):
|
||||
"""Setup view"""
|
||||
|
||||
def get_template_names(self) -> List[str]:
|
||||
allowed = ["dc", "custom", "k8s_manual", "k8s_integration"]
|
||||
setup_type = self.request.GET.get("type", "dc")
|
||||
if setup_type not in allowed:
|
||||
setup_type = allowed[0]
|
||||
return [f"outposts/setup_{setup_type}.html"]
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
outpost: Outpost = get_object_for_user_or_404(
|
||||
self.request.user,
|
||||
"passbook_outposts.view_outpost",
|
||||
pk=self.kwargs["outpost_pk"],
|
||||
)
|
||||
kwargs.update(
|
||||
{"host": self.request.build_absolute_uri("/"), "outpost": outpost}
|
||||
)
|
||||
return kwargs
|
|
@ -5,9 +5,11 @@ CELERY_BEAT_SCHEDULE = {
|
|||
"policies_reputation_ip_save": {
|
||||
"task": "passbook.policies.reputation.tasks.save_ip_reputation",
|
||||
"schedule": crontab(minute="*/5"),
|
||||
"options": {"queue": "passbook_scheduled"},
|
||||
},
|
||||
"policies_reputation_user_save": {
|
||||
"task": "passbook.policies.reputation.tasks.save_user_reputation",
|
||||
"schedule": crontab(minute="*/5"),
|
||||
"options": {"queue": "passbook_scheduled"},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -236,7 +236,7 @@ class OAuth2Provider(Provider):
|
|||
return OAuth2ProviderForm
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return f"OAuth2 Provider {self.name}"
|
||||
|
||||
def html_setup_urls(self, request: HttpRequest) -> Optional[str]:
|
||||
"""return template and context modal with URLs for authorize, token, openid-config, etc"""
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
"""passbook OAuth2 OpenID well-known views"""
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, reverse
|
||||
from django.views import View
|
||||
|
@ -16,6 +18,41 @@ PLAN_CONTEXT_SCOPES = "scopes"
|
|||
class ProviderInfoView(View):
|
||||
"""OpenID-compliant Provider Info"""
|
||||
|
||||
def get_info(self, provider: OAuth2Provider) -> Dict[str, Any]:
|
||||
"""Get dictionary for OpenID Connect information"""
|
||||
return {
|
||||
"issuer": provider.get_issuer(self.request),
|
||||
"authorization_endpoint": self.request.build_absolute_uri(
|
||||
reverse("passbook_providers_oauth2:authorize")
|
||||
),
|
||||
"token_endpoint": self.request.build_absolute_uri(
|
||||
reverse("passbook_providers_oauth2:token")
|
||||
),
|
||||
"userinfo_endpoint": self.request.build_absolute_uri(
|
||||
reverse("passbook_providers_oauth2:userinfo")
|
||||
),
|
||||
"end_session_endpoint": self.request.build_absolute_uri(
|
||||
reverse("passbook_providers_oauth2:end-session")
|
||||
),
|
||||
"introspection_endpoint": self.request.build_absolute_uri(
|
||||
reverse("passbook_providers_oauth2:token-introspection")
|
||||
),
|
||||
"response_types_supported": [provider.response_type],
|
||||
"jwks_uri": self.request.build_absolute_uri(
|
||||
reverse(
|
||||
"passbook_providers_oauth2:jwks",
|
||||
kwargs={"application_slug": provider.application.slug},
|
||||
)
|
||||
),
|
||||
"id_token_signing_alg_values_supported": [provider.jwt_alg],
|
||||
# See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
|
||||
"subject_types_supported": ["public"],
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"client_secret_post",
|
||||
"client_secret_basic",
|
||||
],
|
||||
}
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get(
|
||||
self, request: HttpRequest, application_slug: str, *args, **kwargs
|
||||
|
@ -26,40 +63,7 @@ class ProviderInfoView(View):
|
|||
provider: OAuth2Provider = get_object_or_404(
|
||||
OAuth2Provider, pk=application.provider_id
|
||||
)
|
||||
response = JsonResponse(
|
||||
{
|
||||
"issuer": provider.get_issuer(request),
|
||||
"authorization_endpoint": request.build_absolute_uri(
|
||||
reverse("passbook_providers_oauth2:authorize")
|
||||
),
|
||||
"token_endpoint": request.build_absolute_uri(
|
||||
reverse("passbook_providers_oauth2:token")
|
||||
),
|
||||
"userinfo_endpoint": request.build_absolute_uri(
|
||||
reverse("passbook_providers_oauth2:userinfo")
|
||||
),
|
||||
"end_session_endpoint": request.build_absolute_uri(
|
||||
reverse("passbook_providers_oauth2:end-session")
|
||||
),
|
||||
"introspection_endpoint": request.build_absolute_uri(
|
||||
reverse("passbook_providers_oauth2:token-introspection")
|
||||
),
|
||||
"response_types_supported": [provider.response_type],
|
||||
"jwks_uri": request.build_absolute_uri(
|
||||
reverse(
|
||||
"passbook_providers_oauth2:jwks",
|
||||
kwargs={"application_slug": application.slug},
|
||||
)
|
||||
),
|
||||
"id_token_signing_alg_values_supported": [provider.jwt_alg],
|
||||
# See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
|
||||
"subject_types_supported": ["public"],
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"client_secret_post",
|
||||
"client_secret_basic",
|
||||
],
|
||||
}
|
||||
)
|
||||
response = JsonResponse(self.get_info(provider))
|
||||
response["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
return response
|
||||
|
|
|
@ -1,10 +1,38 @@
|
|||
"""ProxyProvider API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, Serializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from passbook.providers.oauth2.views.provider import ProviderInfoView
|
||||
from passbook.providers.proxy.models import ProxyProvider
|
||||
|
||||
|
||||
class OpenIDConnectConfigurationSerializer(Serializer):
|
||||
"""rest_framework Serializer for OIDC Configuration"""
|
||||
|
||||
issuer = CharField()
|
||||
authorization_endpoint = CharField()
|
||||
token_endpoint = CharField()
|
||||
userinfo_endpoint = CharField()
|
||||
end_session_endpoint = CharField()
|
||||
introspection_endpoint = CharField()
|
||||
jwks_uri = CharField()
|
||||
|
||||
response_types_supported = ListField(child=CharField())
|
||||
id_token_signing_alg_values_supported = ListField(child=CharField())
|
||||
subject_types_supported = ListField(child=CharField())
|
||||
token_endpoint_auth_methods_supported = ListField(child=CharField())
|
||||
|
||||
def create(self, request: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ProxyProviderSerializer(ModelSerializer):
|
||||
"""ProxyProvider Serializer"""
|
||||
|
||||
|
@ -21,7 +49,13 @@ class ProxyProviderSerializer(ModelSerializer):
|
|||
class Meta:
|
||||
|
||||
model = ProxyProvider
|
||||
fields = ["pk", "name", "internal_host", "external_host"]
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"internal_host",
|
||||
"external_host",
|
||||
"certificate",
|
||||
]
|
||||
|
||||
|
||||
class ProxyProviderViewSet(ModelViewSet):
|
||||
|
@ -29,3 +63,47 @@ class ProxyProviderViewSet(ModelViewSet):
|
|||
|
||||
queryset = ProxyProvider.objects.all()
|
||||
serializer_class = ProxyProviderSerializer
|
||||
|
||||
|
||||
class ProxyOutpostConfigSerializer(ModelSerializer):
|
||||
"""ProxyProvider Serializer"""
|
||||
|
||||
oidc_configuration = SerializerMethodField()
|
||||
|
||||
def create(self, validated_data):
|
||||
instance: ProxyProvider = super().create(validated_data)
|
||||
instance.set_oauth_defaults()
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def update(self, instance: ProxyProvider, validated_data):
|
||||
instance.set_oauth_defaults()
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ProxyProvider
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"internal_host",
|
||||
"external_host",
|
||||
"client_id",
|
||||
"client_secret",
|
||||
"oidc_configuration",
|
||||
"cookie_secret",
|
||||
"certificate",
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=OpenIDConnectConfigurationSerializer)
|
||||
def get_oidc_configuration(self, obj: ProxyProvider):
|
||||
"""Embed OpenID Connect provider information"""
|
||||
# pylint: disable=protected-access
|
||||
return ProviderInfoView(request=self.context["request"]._request).get_info(obj)
|
||||
|
||||
|
||||
class OutpostConfigViewSet(ModelViewSet):
|
||||
"""ProxyProvider Viewset"""
|
||||
|
||||
queryset = ProxyProvider.objects.filter(application__isnull=False)
|
||||
serializer_class = ProxyOutpostConfigSerializer
|
||||
|
|
|
@ -8,4 +8,3 @@ class PassbookProviderProxyConfig(AppConfig):
|
|||
name = "passbook.providers.proxy"
|
||||
label = "passbook_providers_proxy"
|
||||
verbose_name = "passbook Providers.Proxy"
|
||||
mountpoint = "application/proxy/"
|
||||
|
|
0
passbook/providers/proxy/controllers/__init__.py
Normal file
0
passbook/providers/proxy/controllers/__init__.py
Normal file
13
passbook/providers/proxy/controllers/kubernetes.py
Normal file
13
passbook/providers/proxy/controllers/kubernetes.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
"""Proxy Provider Kubernetes Contoller"""
|
||||
from passbook.outposts.controllers.kubernetes import KubernetesController
|
||||
|
||||
|
||||
class ProxyKubernetesController(KubernetesController):
|
||||
"""Proxy Provider Kubernetes Contoller"""
|
||||
|
||||
def __init__(self, outpost_pk: str):
|
||||
super().__init__(outpost_pk)
|
||||
self.deployment_ports = {
|
||||
"http": 4180,
|
||||
"https": 4443,
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
"""passbook Proxy Provider Forms"""
|
||||
from django import forms
|
||||
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.flows.models import Flow, FlowDesignation
|
||||
from passbook.providers.proxy.models import ProxyProvider
|
||||
|
||||
|
||||
|
@ -9,14 +11,31 @@ class ProxyProviderForm(forms.ModelForm):
|
|||
|
||||
instance: ProxyProvider
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["authorization_flow"].queryset = Flow.objects.filter(
|
||||
designation=FlowDesignation.AUTHORIZATION
|
||||
)
|
||||
self.fields["certificate"].queryset = CertificateKeyPair.objects.filter(
|
||||
key_data__isnull=False
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
actual_save = super().save(*args, **kwargs)
|
||||
self.instance.set_oauth_defaults()
|
||||
return super().save(*args, **kwargs)
|
||||
self.instance.save()
|
||||
return actual_save
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ProxyProvider
|
||||
fields = ["name", "authorization_flow", "internal_host", "external_host"]
|
||||
fields = [
|
||||
"name",
|
||||
"authorization_flow",
|
||||
"internal_host",
|
||||
"external_host",
|
||||
"certificate",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"internal_host": forms.TextInput(),
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 3.1 on 2020-08-19 14:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import passbook.providers.proxy.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_providers_proxy", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="proxyprovider",
|
||||
name="cookie_secret",
|
||||
field=models.TextField(
|
||||
default=passbook.providers.proxy.models.get_cookie_secret
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.1 on 2020-08-23 22:46
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_crypto", "0002_create_self_signed_kp"),
|
||||
("passbook_providers_proxy", "0002_proxyprovider_cookie_secret"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="proxyprovider",
|
||||
name="certificate",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="passbook_crypto.certificatekeypair",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,13 +1,16 @@
|
|||
"""passbook proxy models"""
|
||||
from typing import Optional, Type
|
||||
import string
|
||||
from random import SystemRandom
|
||||
from typing import Iterable, Type
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.core.validators import URLValidator
|
||||
from django.db import models
|
||||
from django.forms import ModelForm
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.lib.utils.template import render_to_string
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.outposts.models import OutpostModel
|
||||
from passbook.providers.oauth2.constants import (
|
||||
SCOPE_OPENID,
|
||||
SCOPE_OPENID_EMAIL,
|
||||
|
@ -22,7 +25,18 @@ from passbook.providers.oauth2.models import (
|
|||
)
|
||||
|
||||
|
||||
class ProxyProvider(OAuth2Provider):
|
||||
def get_cookie_secret():
|
||||
"""Generate random 32-character string for cookie-secret"""
|
||||
return "".join(
|
||||
SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32)
|
||||
)
|
||||
|
||||
|
||||
def _get_callback_url(uri: str) -> str:
|
||||
return urljoin(uri, "/pbprox/callback")
|
||||
|
||||
|
||||
class ProxyProvider(OutpostModel, OAuth2Provider):
|
||||
"""Protect applications that don't support any of the other
|
||||
Protocols by using a Reverse-Proxy."""
|
||||
|
||||
|
@ -33,39 +47,42 @@ class ProxyProvider(OAuth2Provider):
|
|||
validators=[URLValidator(schemes=("http", "https"))]
|
||||
)
|
||||
|
||||
cookie_secret = models.TextField(default=get_cookie_secret)
|
||||
|
||||
certificate = models.ForeignKey(
|
||||
CertificateKeyPair, on_delete=models.SET_NULL, null=True
|
||||
)
|
||||
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from passbook.providers.proxy.forms import ProxyProviderForm
|
||||
|
||||
return ProxyProviderForm
|
||||
|
||||
def html_setup_urls(self, request: HttpRequest) -> Optional[str]:
|
||||
"""return template and context modal with URLs for authorize, token, openid-config, etc"""
|
||||
from passbook.providers.proxy.views import DockerComposeView
|
||||
|
||||
docker_compose_yaml = DockerComposeView(request=request).get_compose(self)
|
||||
return render_to_string(
|
||||
"providers/proxy/setup_modal.html",
|
||||
{"provider": self, "docker_compose": docker_compose_yaml},
|
||||
)
|
||||
|
||||
def set_oauth_defaults(self):
|
||||
"""Ensure all OAuth2-related settings are correct"""
|
||||
self.client_type = ClientTypes.CONFIDENTIAL
|
||||
self.response_type = ResponseTypes.CODE
|
||||
self.jwt_alg = JWTAlgorithms.HS256
|
||||
self.jwt_alg = JWTAlgorithms.RS256
|
||||
self.rsa_key = CertificateKeyPair.objects.first()
|
||||
scopes = ScopeMapping.objects.filter(
|
||||
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_PROFILE, SCOPE_OPENID_EMAIL]
|
||||
)
|
||||
self.property_mappings.set(scopes)
|
||||
self.redirect_uris = "\n".join(
|
||||
[
|
||||
f"{self.external_host}/oauth2/callback",
|
||||
f"{self.internal_host}/oauth2/callback",
|
||||
_get_callback_url(self.external_host),
|
||||
_get_callback_url(self.internal_host),
|
||||
]
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return f"Proxy Provider {self.name}"
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model]:
|
||||
required_models = [self]
|
||||
if self.certificate is not None:
|
||||
required_models.append(self.certificate)
|
||||
return required_models
|
||||
|
||||
class Meta:
|
||||
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: "passbook-gatekeeper-{{ provider.name }}"
|
||||
passbook.beryju.org/gatekeeper/provider: "{{ provider.pk }}"
|
||||
name: passbook-gatekeeper
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: passbook-gatekeeper
|
||||
passbook.beryju.org/gatekeeper/provider: "{{ provider.pk }}"
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: passbook-gatekeeper
|
||||
passbook.beryju.org/gatekeeper/provider: "{{ provider.pk }}"
|
||||
spec:
|
||||
containers:
|
||||
- args:
|
||||
- --upstream=file:///dev/null
|
||||
env:
|
||||
- name: OAUTH2_PROXY_CLIENT_ID
|
||||
value: "{{ provider.client.client_id }}"
|
||||
- name: OAUTH2_PROXY_CLIENT_SECRET
|
||||
value: "{{ provider.client.client_secret }}"
|
||||
- name: OAUTH2_PROXY_COOKIE_SECRET
|
||||
value: "{{ cookie_secret }}"
|
||||
- name: OAUTH2_PROXY_OIDC_ISSUER_URL
|
||||
value: "{{ issuer }}"
|
||||
- name: OAUTH2_PROXY_SET_XAUTHREQUEST
|
||||
value: "true"
|
||||
- name: OAUTH2_PROXY_SET_AUTHORIZATION_HEADER
|
||||
value: "true"
|
||||
image: beryju/passbook-gatekeeper:{{ version }}
|
||||
imagePullPolicy: Always
|
||||
name: passbook-gatekeeper
|
||||
ports:
|
||||
- containerPort: 4180
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: "passbook-gatekeeper-{{ provider.name }}"
|
||||
passbook.beryju.org/gatekeeper/provider: "{{ provider.pk }}"
|
||||
name: passbook-gatekeeper
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 4180
|
||||
protocol: TCP
|
||||
targetPort: 4180
|
||||
selector:
|
||||
app.kubernetes.io/name: passbook-gatekeeper
|
||||
passbook.beryju.org/gatekeeper/provider: "{{ provider.pk }}"
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: passbook-gatekeeper-{{ provider.name }}
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ provider.external_host }}
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: "passbook-gatekeeper-{{ provider.name }}"
|
||||
servicePort: 4180
|
||||
path: /oauth2
|
|
@ -1,10 +0,0 @@
|
|||
"""passbook proxy urls"""
|
||||
from django.urls import path
|
||||
|
||||
from passbook.providers.proxy.views import K8sManifestView
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"<int:provider>/k8s-manifest/", K8sManifestView.as_view(), name="k8s-manifest"
|
||||
),
|
||||
]
|
|
@ -1,96 +0,0 @@
|
|||
"""passbook proxy views"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Model
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.views import View
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from structlog import get_logger
|
||||
from yaml import safe_dump
|
||||
|
||||
from passbook import __version__
|
||||
from passbook.core.models import User
|
||||
from passbook.providers.proxy.models import ProxyProvider
|
||||
|
||||
ORIGINAL_URL = "HTTP_X_ORIGINAL_URL"
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def get_object_for_user_or_404(user: User, perm: str, **filters) -> Model:
|
||||
"""Wrapper that combines get_objects_for_user and get_object_or_404"""
|
||||
return get_object_or_404(get_objects_for_user(user, perm), **filters)
|
||||
|
||||
|
||||
def get_cookie_secret():
|
||||
"""Generate random 32-character string for cookie-secret"""
|
||||
return "".join(
|
||||
SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32)
|
||||
)
|
||||
|
||||
|
||||
class DockerComposeView(LoginRequiredMixin, View):
|
||||
"""Generate docker-compose yaml"""
|
||||
|
||||
def get_compose(self, provider: ProxyProvider) -> str:
|
||||
"""Generate docker-compose yaml, version 3.5"""
|
||||
issuer = provider.get_issuer(self.request)
|
||||
env = {
|
||||
"OAUTH2_PROXY_CLIENT_ID": provider.client_id,
|
||||
"OAUTH2_PROXY_CLIENT_SECRET": provider.client_secret,
|
||||
"OAUTH2_PROXY_REDIRECT_URL": f"{provider.external_host}/oauth2/callback",
|
||||
"OAUTH2_PROXY_OIDC_ISSUER_URL": issuer,
|
||||
"OAUTH2_PROXY_COOKIE_SECRET": get_cookie_secret(),
|
||||
"OAUTH2_PROXY_UPSTREAMS": provider.internal_host,
|
||||
}
|
||||
if urlparse(provider.external_host).scheme != "https":
|
||||
env["OAUTH2_PROXY_COOKIE_SECURE"] = "false"
|
||||
compose = {
|
||||
"version": "3.5",
|
||||
"services": {
|
||||
"passbook_gatekeeper": {
|
||||
"image": f"beryju/passbook-proxy:{__version__}",
|
||||
"ports": ["4180:4180"],
|
||||
"environment": env,
|
||||
}
|
||||
},
|
||||
}
|
||||
return safe_dump(compose, default_flow_style=False)
|
||||
|
||||
def get(self, request: HttpRequest, provider_pk: int) -> HttpResponse:
|
||||
"""Render docker-compose file"""
|
||||
provider: ProxyProvider = get_object_for_user_or_404(
|
||||
request.user,
|
||||
"passbook_providers_proxy.view_applicationgatewayprovider",
|
||||
pk=provider_pk,
|
||||
)
|
||||
response = HttpResponse()
|
||||
response.content_type = "application/x-yaml"
|
||||
response.content = self.get_compose(provider.pk)
|
||||
return response
|
||||
|
||||
|
||||
class K8sManifestView(LoginRequiredMixin, View):
|
||||
"""Generate K8s Deployment and SVC for gatekeeper"""
|
||||
|
||||
def get(self, request: HttpRequest, provider_pk: int) -> HttpResponse:
|
||||
"""Render deployment template"""
|
||||
provider: ProxyProvider = get_object_for_user_or_404(
|
||||
request.user,
|
||||
"passbook_providers_app_gw.view_applicationgatewayprovider",
|
||||
pk=provider_pk,
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"providers/proxy/k8s-manifest.yaml",
|
||||
{
|
||||
"provider": provider,
|
||||
"cookie_secret": get_cookie_secret(),
|
||||
"version": __version__,
|
||||
"issuer": provider.get_issuer(request),
|
||||
},
|
||||
content_type="text/yaml",
|
||||
)
|
|
@ -108,7 +108,7 @@ class SAMLProvider(Provider):
|
|||
return SAMLProviderForm
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return f"SAML Provider {self.name}"
|
||||
|
||||
def link_download_metadata(self):
|
||||
"""Get link to download XML metadata for admin interface"""
|
||||
|
|
120
passbook/root/asgi.py
Normal file
120
passbook/root/asgi.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
"""
|
||||
ASGI config for passbook project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||
"""
|
||||
import os
|
||||
import typing
|
||||
from time import time
|
||||
from typing import Any, ByteString, Dict
|
||||
|
||||
import django
|
||||
from asgiref.compatibility import guarantee_single_callable
|
||||
from channels.routing import get_default_application
|
||||
from defusedxml import defuse_stdlib
|
||||
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
|
||||
from structlog import get_logger
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings")
|
||||
|
||||
defuse_stdlib()
|
||||
django.setup()
|
||||
|
||||
# See https://github.com/encode/starlette/blob/master/starlette/types.py
|
||||
Scope = typing.MutableMapping[str, typing.Any]
|
||||
Message = typing.MutableMapping[str, typing.Any]
|
||||
|
||||
Receive = typing.Callable[[], typing.Awaitable[Message]]
|
||||
Send = typing.Callable[[Message], typing.Awaitable[None]]
|
||||
|
||||
ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]]
|
||||
|
||||
LOGGER = get_logger("passbook.asgi")
|
||||
|
||||
|
||||
class ASGILoggerMiddleware:
|
||||
"""Main ASGI Logger middleware, starts an ASGILogger for each request"""
|
||||
|
||||
def __init__(self, app: ASGIApp) -> None:
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
||||
responder = ASGILogger(self.app)
|
||||
await responder(scope, receive, send)
|
||||
return
|
||||
|
||||
|
||||
class ASGILogger:
|
||||
"""ASGI Logger, instantiated for each request"""
|
||||
|
||||
app: ASGIApp
|
||||
send: Send
|
||||
|
||||
scope: Scope
|
||||
headers: Dict[ByteString, Any]
|
||||
|
||||
status_code: int
|
||||
start: float
|
||||
content_length: int
|
||||
|
||||
def __init__(self, app: ASGIApp):
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
self.send = send
|
||||
self.scope = scope
|
||||
self.content_length = 0
|
||||
self.headers = dict(scope.get("headers", []))
|
||||
|
||||
if self.headers.get(b"host", b"") == b"kubernetes-healthcheck-host":
|
||||
# Don't log kubernetes health/readiness requests
|
||||
await send({"type": "http.response.start", "status": 204, "headers": []})
|
||||
await send({"type": "http.response.body", "body": ""})
|
||||
return
|
||||
|
||||
self.start = time()
|
||||
await self.app(scope, receive, self.send_hooked)
|
||||
|
||||
async def send_hooked(self, message: Message) -> None:
|
||||
"""Hooked send method, which records status code and content-length, and for the final
|
||||
requests logs it"""
|
||||
headers = dict(message.get("headers", []))
|
||||
|
||||
if "status" in message:
|
||||
self.status_code = message["status"]
|
||||
|
||||
if b"Content-Length" in headers:
|
||||
self.content_length += int(headers.get(b"Content-Length", b"0"))
|
||||
|
||||
if message["type"] == "http.response.body" and not message["more_body"]:
|
||||
runtime = int((time() - self.start) * 10 ** 6)
|
||||
self.log(runtime)
|
||||
return await self.send(message)
|
||||
|
||||
def _get_ip(self) -> str:
|
||||
client_ip, _ = self.scope.get("client", ("", 0))
|
||||
return client_ip
|
||||
|
||||
def log(self, runtime: float):
|
||||
"""Outpot access logs in a structured format"""
|
||||
host = self._get_ip()
|
||||
query_string = ""
|
||||
if self.scope.get("query_string", b"") != b"":
|
||||
query_string = f"?{self.scope.get('query_string').decode()}"
|
||||
LOGGER.info(
|
||||
f"{self.scope.get('path', '')}{query_string}",
|
||||
host=host,
|
||||
method=self.scope.get("method", ""),
|
||||
scheme=self.scope.get("scheme", ""),
|
||||
status=self.status_code,
|
||||
size=self.content_length / 1000 if self.content_length > 0 else "-",
|
||||
runtime=runtime,
|
||||
)
|
||||
|
||||
|
||||
application = SentryAsgiMiddleware(
|
||||
ASGILogger(guarantee_single_callable(get_default_application()))
|
||||
)
|
12
passbook/root/routing.py
Normal file
12
passbook/root/routing.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
"""root Websocket URLS"""
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from django.urls import path
|
||||
|
||||
from passbook.outposts.channels import OutpostConsumer
|
||||
|
||||
application = ProtocolTypeRouter(
|
||||
{
|
||||
# (http->django views is added by default)
|
||||
"websocket": URLRouter([path("ws/outpost/<uuid:pk>/", OutpostConsumer)]),
|
||||
}
|
||||
)
|
|
@ -73,11 +73,13 @@ INSTALLED_APPS = [
|
|||
"drf_yasg",
|
||||
"guardian",
|
||||
"django_prometheus",
|
||||
"channels",
|
||||
"passbook.admin.apps.PassbookAdminConfig",
|
||||
"passbook.api.apps.PassbookAPIConfig",
|
||||
"passbook.audit.apps.PassbookAuditConfig",
|
||||
"passbook.crypto.apps.PassbookCryptoConfig",
|
||||
"passbook.flows.apps.PassbookFlowsConfig",
|
||||
"passbook.outposts.apps.PassbookOutpostConfig",
|
||||
"passbook.lib.apps.PassbookLibConfig",
|
||||
"passbook.policies.apps.PassbookPoliciesConfig",
|
||||
"passbook.policies.dummy.apps.PassbookPolicyDummyConfig",
|
||||
|
@ -125,13 +127,13 @@ REST_FRAMEWORK = {
|
|||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
|
||||
"PAGE_SIZE": 100,
|
||||
"DEFAULT_FILTER_BACKENDS": [
|
||||
"rest_framework_guardian.filters.ObjectPermissionsFilter",
|
||||
"django_filters.rest_framework.DjangoFilterBackend",
|
||||
"rest_framework.filters.OrderingFilter",
|
||||
"rest_framework.filters.SearchFilter",
|
||||
],
|
||||
"DEFAULT_PERMISSION_CLASSES": (
|
||||
"rest_framework.permissions.DjangoObjectPermissions",
|
||||
"passbook.api.permissions.CustomObjectPermissions",
|
||||
),
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
"passbook.api.auth.PassbookTokenAuthentication",
|
||||
|
@ -185,7 +187,15 @@ TEMPLATES = [
|
|||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "passbook.root.wsgi.application"
|
||||
ASGI_APPLICATION = "passbook.root.routing.application"
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {"hosts": [(CONFIG.y("redis.host"), 6379)]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
|
||||
|
@ -234,6 +244,7 @@ CELERY_BEAT_SCHEDULE = {
|
|||
"clean_expired_models": {
|
||||
"task": "passbook.core.tasks.clean_expired_models",
|
||||
"schedule": crontab(minute="*/5"), # Run every 5 minutes
|
||||
"options": {"queue": "passbook_scheduled"},
|
||||
}
|
||||
}
|
||||
CELERY_CREATE_MISSING_QUEUES = True
|
||||
|
@ -364,6 +375,7 @@ _LOGGING_HANDLER_MAP = {
|
|||
"grpc": LOG_LEVEL,
|
||||
"docker": "WARNING",
|
||||
"urllib3": "WARNING",
|
||||
"websockets": "WARNING",
|
||||
}
|
||||
for handler_name, level in _LOGGING_HANDLER_MAP.items():
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
"""
|
||||
WSGI config for passbook project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/
|
||||
"""
|
||||
import os
|
||||
from time import time
|
||||
|
||||
from defusedxml import defuse_stdlib
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.lib.utils.http import _get_client_ip_from_meta
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings")
|
||||
defuse_stdlib()
|
||||
|
||||
|
||||
class WSGILogger:
|
||||
""" This is the generalized WSGI middleware for any style request logging. """
|
||||
|
||||
def __init__(self, _application):
|
||||
self.application = _application
|
||||
self.logger = get_logger("passbook.wsgi")
|
||||
|
||||
def __healthcheck(self, start_response):
|
||||
start_response("204 OK", [])
|
||||
return [b""]
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
start = time()
|
||||
status_codes = []
|
||||
content_lengths = []
|
||||
|
||||
if environ.get("HTTP_HOST", "").startswith("kubernetes-healthcheck-host"):
|
||||
# Don't log kubernetes health/readiness requests
|
||||
return self.__healthcheck(start_response)
|
||||
|
||||
def custom_start_response(status, response_headers, exc_info=None):
|
||||
status_codes.append(int(status.partition(" ")[0]))
|
||||
for name, value in response_headers:
|
||||
if name.lower() == "content-length":
|
||||
content_lengths.append(int(value))
|
||||
break
|
||||
return start_response(status, response_headers, exc_info)
|
||||
|
||||
retval = self.application(environ, custom_start_response)
|
||||
runtime = int((time() - start) * 10 ** 6)
|
||||
content_length = content_lengths[0] if content_lengths else 0
|
||||
self.log(status_codes[0], environ, content_length, runtime=runtime)
|
||||
return retval
|
||||
|
||||
def log(self, status_code, environ, content_length, **kwargs):
|
||||
"""
|
||||
Apache log format 'NCSA extended/combined log':
|
||||
"%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\""
|
||||
see http://httpd.apache.org/docs/current/mod/mod_log_config.html#formats
|
||||
"""
|
||||
host = _get_client_ip_from_meta(environ)
|
||||
query_string = ""
|
||||
if environ.get("QUERY_STRING") != "":
|
||||
query_string = f"?{environ.get('QUERY_STRING')}"
|
||||
self.logger.info(
|
||||
"request",
|
||||
path=f"{environ.get('PATH_INFO', '')}{query_string}",
|
||||
host=host,
|
||||
method=environ.get("REQUEST_METHOD", ""),
|
||||
protocol=environ.get("SERVER_PROTOCOL", ""),
|
||||
status=status_code,
|
||||
size=content_length / 1000 if content_length > 0 else "-",
|
||||
runtime=kwargs.get("runtime"),
|
||||
)
|
||||
|
||||
|
||||
application = WSGILogger(get_wsgi_application())
|
|
@ -9,5 +9,6 @@ CELERY_BEAT_SCHEDULE = {
|
|||
"sources_ldap_sync": {
|
||||
"task": "passbook.sources.ldap.tasks.sync",
|
||||
"schedule": crontab(minute=0), # Run every hour
|
||||
"options": {"queue": "passbook_scheduled"},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,5 +5,6 @@ CELERY_BEAT_SCHEDULE = {
|
|||
"saml_source_cleanup": {
|
||||
"task": "passbook.sources.saml.tasks.clean_temporary_users",
|
||||
"schedule": crontab(minute="*/5"),
|
||||
"options": {"queue": "passbook_scheduled"},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 3.1 on 2020-08-28 13:14
|
||||
# Generated by Django 3.1 on 2020-08-23 22:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
2
proxy/.dockerignore
Normal file
2
proxy/.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
Dockerfile.*
|
||||
.git
|
2
proxy/.gitignore
vendored
Normal file
2
proxy/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
pkg/client/
|
||||
pkg/models/
|
12
proxy/Dockerfile
Normal file
12
proxy/Dockerfile
Normal file
|
@ -0,0 +1,12 @@
|
|||
FROM golang:1.15 AS builder
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go build -o /work/proxy .
|
||||
|
||||
# Copy binary to alpine
|
||||
FROM gcr.io/distroless/base-debian10
|
||||
COPY --from=builder /work/proxy /
|
||||
ENTRYPOINT ["/proxy"]
|
6
proxy/Makefile
Normal file
6
proxy/Makefile
Normal file
|
@ -0,0 +1,6 @@
|
|||
generate:
|
||||
go get -u github.com/go-swagger/go-swagger/cmd/swagger
|
||||
swagger generate client -f ../swagger.yaml -A passbook -t pkg/
|
||||
|
||||
run:
|
||||
go run -v .
|
24
proxy/README.md
Normal file
24
proxy/README.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
# passbook Proxy
|
||||
|
||||
[![CI Build status](https://img.shields.io/azure-devops/build/beryjuorg/passbook/3?style=flat-square)](https://dev.azure.com/beryjuorg/passbook/_build?definitionId=3)
|
||||
![Docker pulls (proxy)](https://img.shields.io/docker/pulls/beryju/passbook-proxy.svg?style=flat-square)
|
||||
|
||||
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 a new issue