diff --git a/Dockerfile b/Dockerfile
index b2fdfbd..763c353 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -23,7 +23,8 @@ WORKDIR /$appname
RUN python -m pip install --upgrade pip \
&& pip install pipenv \
- && pipenv install --system --deploy
+ && pipenv install --system --deploy \
+ && pip freeze
RUN mkdir -p /var/www/$appname \
&& mkdir -p /var/www/.cache/Python-Eggs/ \
diff --git a/Pipfile b/Pipfile
index 26c56b1..93aff3c 100644
--- a/Pipfile
+++ b/Pipfile
@@ -6,6 +6,7 @@ verify_ssl = true
[dev-packages]
codacy-coverage = "*"
truffleHog = "*"
+mock = "~=1.0"
PyGithub = "*"
pytest = ">=3.2.3"
pytest-cov = ">=2.5.1"
@@ -13,12 +14,12 @@ pytest-flask = ">=0.10.0"
PyYAML = ">=3.13"
[packages]
+alembic = ">=1.4.1"
requests = "*"
-flasgger = "*"
flask-cors = "~=3.0"
cdiserrors = "~=0.1"
cdislogging = "~=0.0"
-Authlib = "==0.4.1"
+Authlib = "==0.11"
cryptography = "~=2.3"
Flask = "~=1.0"
Flask-SQLAlchemy = "~=2.3"
@@ -26,7 +27,7 @@ psycopg2 = "~=2.7"
python-jose = "~=3.0"
kubernetes = "~=6.0"
pyyaml = "==4.2b1"
-authutils = "*"
+authutils = "==4.0.0"
[requires]
python_version = "3.6"
diff --git a/Pipfile.lock b/Pipfile.lock
index dacaa68..670a354 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "570552dacdaef32d13c9345c49e0daf6116c1b0b902c698970089a34408c25e2"
+ "sha256": "ec7357efb28eb2fbdbbcb1afec9002df580a647b9fe0ce2343daf575e78067dc"
},
"pipfile-spec": 6,
"requires": {
@@ -23,6 +23,13 @@
],
"version": "==2.2.1"
},
+ "alembic": {
+ "hashes": [
+ "sha256:791a5686953c4b366d3228c5377196db2f534475bb38d26f70eb69668efd9028"
+ ],
+ "index": "pypi",
+ "version": "==1.4.1"
+ },
"argparse": {
"hashes": [
"sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4",
@@ -30,41 +37,27 @@
],
"version": "==1.4.0"
},
- "asn1crypto": {
- "hashes": [
- "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87",
- "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"
- ],
- "version": "==0.24.0"
- },
- "attrs": {
- "hashes": [
- "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
- "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
- ],
- "version": "==19.1.0"
- },
"authlib": {
"hashes": [
- "sha256:3bd0591941f5f2eb86d1b1438df514c9c64028a32dcafb816b2cf784f4c6d727",
- "sha256:85af01b717402484451fb03b711d667a919c53f800ab28c552431c20770ef159"
+ "sha256:3a226f231e962a16dd5f6fcf0c113235805ba206e294717a64fa8e04ae3ad9c4",
+ "sha256:9741db6de2950a0a5cefbdb72ec7ab12f7e9fd530ff47219f1530e79183cbaaf"
],
"index": "pypi",
- "version": "==0.4.1"
+ "version": "==0.11"
},
"authutils": {
"hashes": [
- "sha256:8311a3ce799b66705e315b798653c95ff9091ef21e58dc34b0fc0e9207c35f75"
+ "sha256:d67cd5025d8584a23a3ba583719e6116807f13450b611d1d82b112cdb13a1631"
],
"index": "pypi",
- "version": "==3.1.0"
+ "version": "==4.0.0"
},
"babel": {
"hashes": [
- "sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab",
- "sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"
+ "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38",
+ "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"
],
- "version": "==2.7.0"
+ "version": "==2.8.0"
},
"cached-property": {
"hashes": [
@@ -75,10 +68,10 @@
},
"cachetools": {
"hashes": [
- "sha256:428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae",
- "sha256:8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a"
+ "sha256:9a52dd97a85f257f4e4127f15818e71a0c7899f121b34591fcc1173ea79a0198",
+ "sha256:b304586d357c43221856be51d73387f93e2a961598a9b6b6670664746f3b6c6c"
],
- "version": "==3.1.1"
+ "version": "==4.0.0"
},
"cdiserrors": {
"hashes": [
@@ -96,43 +89,43 @@
},
"certifi": {
"hashes": [
- "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
- "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
+ "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
+ "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
],
- "version": "==2019.3.9"
+ "version": "==2019.11.28"
},
"cffi": {
"hashes": [
- "sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774",
- "sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d",
- "sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90",
- "sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b",
- "sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63",
- "sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45",
- "sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25",
- "sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3",
- "sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b",
- "sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647",
- "sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016",
- "sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4",
- "sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb",
- "sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753",
- "sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7",
- "sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9",
- "sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f",
- "sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8",
- "sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f",
- "sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc",
- "sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42",
- "sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3",
- "sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909",
- "sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45",
- "sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d",
- "sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512",
- "sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff",
- "sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201"
- ],
- "version": "==1.12.3"
+ "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
+ "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
+ "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
+ "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
+ "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
+ "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
+ "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
+ "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
+ "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
+ "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
+ "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
+ "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
+ "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
+ "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
+ "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
+ "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
+ "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
+ "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
+ "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
+ "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
+ "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
+ "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
+ "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
+ "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
+ "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
+ "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
+ "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
+ "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
+ ],
+ "version": "==1.14.0"
},
"chardet": {
"hashes": [
@@ -143,62 +136,59 @@
},
"click": {
"hashes": [
- "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
- "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
+ "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
+ "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
],
- "version": "==7.0"
+ "version": "==7.1.1"
},
"cryptography": {
"hashes": [
- "sha256:24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c",
- "sha256:25dd1581a183e9e7a806fe0543f485103232f940fcfc301db65e630512cce643",
- "sha256:3452bba7c21c69f2df772762be0066c7ed5dc65df494a1d53a58b683a83e1216",
- "sha256:41a0be220dd1ed9e998f5891948306eb8c812b512dc398e5a01846d855050799",
- "sha256:5751d8a11b956fbfa314f6553d186b94aa70fdb03d8a4d4f1c82dcacf0cbe28a",
- "sha256:5f61c7d749048fa6e3322258b4263463bfccefecb0dd731b6561cb617a1d9bb9",
- "sha256:72e24c521fa2106f19623a3851e9f89ddfdeb9ac63871c7643790f872a305dfc",
- "sha256:7b97ae6ef5cba2e3bb14256625423413d5ce8d1abb91d4f29b6d1a081da765f8",
- "sha256:961e886d8a3590fd2c723cf07be14e2a91cf53c25f02435c04d39e90780e3b53",
- "sha256:96d8473848e984184b6728e2c9d391482008646276c3ff084a1bd89e15ff53a1",
- "sha256:ae536da50c7ad1e002c3eee101871d93abdc90d9c5f651818450a0d3af718609",
- "sha256:b0db0cecf396033abb4a93c95d1602f268b3a68bb0a9cc06a7cff587bb9a7292",
- "sha256:cfee9164954c186b191b91d4193989ca994703b2fff406f71cf454a2d3c7327e",
- "sha256:e6347742ac8f35ded4a46ff835c60e68c22a536a8ae5c4422966d06946b6d4c6",
- "sha256:f27d93f0139a3c056172ebb5d4f9056e770fdf0206c2f422ff2ebbad142e09ed",
- "sha256:f57b76e46a58b63d1c6375017f4564a28f19a5ca912691fd2e4261b3414b618d"
+ "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c",
+ "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595",
+ "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad",
+ "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651",
+ "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2",
+ "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff",
+ "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d",
+ "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42",
+ "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d",
+ "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e",
+ "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912",
+ "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793",
+ "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13",
+ "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7",
+ "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0",
+ "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879",
+ "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f",
+ "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9",
+ "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2",
+ "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf",
+ "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"
],
"index": "pypi",
- "version": "==2.7"
+ "version": "==2.8"
},
"debtcollector": {
"hashes": [
- "sha256:721b508130c2f133dcc14145c1e213967a84e31a15619b73d51dee79baef7f54",
- "sha256:f6ce5a383ad73c23e1138dbb69bf45d33f4a4bdec38f02dbf2b89477ec5e55bc"
+ "sha256:7a2e4fe10f938a15e888de3f8a00ba80c02b494d37c5e724dabb8a5a530880a5",
+ "sha256:bdef71fc362fadfde363d78c08820dfac38757bc99ebf2bf3cae72f6d93d1f60"
],
- "version": "==1.21.0"
+ "version": "==2.0.0"
},
"ecdsa": {
"hashes": [
- "sha256:20c17e527e75acad8f402290e158a6ac178b91b881f941fc6ea305bfdfb9657c",
- "sha256:5c034ffa23413ac923541ceb3ac14ec15a0d2530690413bff58c12b80e56d884"
- ],
- "version": "==0.13.2"
- },
- "flasgger": {
- "hashes": [
- "sha256:db3c211ca78472a71ff8578c069de6e955be04a047bfbc4fab45743d804a0d57",
- "sha256:e799c77d6b777356a0fc902ced16f4e5f5c518e2d96748da7644bcb59b67153b"
+ "sha256:867ec9cf6df0b03addc8ef66b56359643cb5d0c1dc329df76ba7ecfe256c8061",
+ "sha256:8f12ac317f8a1318efa75757ef0a651abe12e51fc1af8838fb91079445227277"
],
- "index": "pypi",
- "version": "==0.9.2"
+ "version": "==0.15"
},
"flask": {
"hashes": [
- "sha256:ad7c6d841e64296b962296c2c2dabc6543752985727af86a975072dea984b6f3",
- "sha256:e7d32475d1de5facaa55e3958bc4ec66d3762076b074296aa50ef8fdc5b9df61"
+ "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
+ "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
],
"index": "pypi",
- "version": "==1.0.3"
+ "version": "==1.1.1"
},
"flask-cors": {
"hashes": [
@@ -210,38 +200,32 @@
},
"flask-sqlalchemy": {
"hashes": [
- "sha256:0c9609b0d72871c540a7945ea559c8fdf5455192d2db67219509aed680a3d45a",
- "sha256:8631bbea987bc3eb0f72b1f691d47bd37ceb795e73b59ab48586d76d75a7c605"
+ "sha256:0078d8663330dc05a74bc72b3b6ddc441b9a744e2f56fe60af1a5bfc81334327",
+ "sha256:6974785d913666587949f7c2946f7001e4fa2cb2d19f4e69ead02e4b8f50b33d"
],
"index": "pypi",
- "version": "==2.4.0"
- },
- "future": {
- "hashes": [
- "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"
- ],
- "version": "==0.17.1"
+ "version": "==2.4.1"
},
"google-auth": {
"hashes": [
- "sha256:0f7c6a64927d34c1a474da92cfc59e552a5d3b940d3266606c6a28b72888b9e4",
- "sha256:20705f6803fd2c4d1cc2dcb0df09d4dfcb9a7d51fd59e94a3a28231fd93119ed"
+ "sha256:4fddaf62bcfc3b9cc1bb2062130937a25ebe781b8eb15beec217c160b8cabb68",
+ "sha256:ec172006e626bb90f6069e9358c373bc991a15da6cc55276986d9ecd29235b15"
],
- "version": "==1.6.3"
+ "version": "==1.11.3"
},
"idna": {
"hashes": [
- "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
- "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
+ "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
+ "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
],
- "version": "==2.8"
+ "version": "==2.9"
},
"ipaddress": {
"hashes": [
- "sha256:64b28eec5e78e7510698f6d4da08800a5c575caa4a286c93d651c5d3ff7b6794",
- "sha256:b146c751ea45cad6188dd6cf2d9b757f6f4f8d6ffb96a023e6f2e26eea02a72c"
+ "sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc",
+ "sha256:b7f8e0369580bb4a24d5ba1d7cc29660a4a6987763faf1d8a8046830e020e7e2"
],
- "version": "==1.0.22"
+ "version": "==1.0.23"
},
"iso8601": {
"hashes": [
@@ -260,17 +244,10 @@
},
"jinja2": {
"hashes": [
- "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
- "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
- ],
- "version": "==2.10.1"
- },
- "jsonschema": {
- "hashes": [
- "sha256:0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d",
- "sha256:a5f6559964a3851f59040d3b961de5e68e70971afb88ba519d27e6a039efff1a"
+ "sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484",
+ "sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668"
],
- "version": "==3.0.1"
+ "version": "==3.0.0a1"
},
"kubernetes": {
"hashes": [
@@ -280,19 +257,29 @@
"index": "pypi",
"version": "==6.1.0"
},
+ "mako": {
+ "hashes": [
+ "sha256:3139c5d64aa5d175dbafb95027057128b5fbd05a40c53999f3905ceb53366d9d",
+ "sha256:8e8b53c71c7e59f3de716b6832c4e401d903af574f6962edbbbf6ecc2a5fe6c9"
+ ],
+ "version": "==1.1.2"
+ },
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
+ "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
+ "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
+ "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
@@ -309,38 +296,34 @@
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
- "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
+ "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
+ "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
+ "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
"version": "==1.1.1"
},
- "mistune": {
- "hashes": [
- "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e",
- "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"
- ],
- "version": "==0.8.4"
- },
"msgpack": {
"hashes": [
- "sha256:26cb40116111c232bc235ce131cc3b4e76549088cb154e66a2eb8ff6fcc907ec",
- "sha256:300fd3f2c664a3bf473d6a952f843b4a71454f4c592ed7e74a36b205c1782d28",
- "sha256:3129c355342853007de4a2a86e75eab966119733eb15748819b6554363d4e85c",
- "sha256:31f6d645ee5a97d59d3263fab9e6be76f69fa131cddc0d94091a3c8aca30d67a",
- "sha256:3ce7ef7ee2546c3903ca8c934d09250531b80c6127e6478781ae31ed835aac4c",
- "sha256:4008c72f5ef2b7936447dcb83db41d97e9791c83221be13d5e19db0796df1972",
- "sha256:62bd8e43d204580308d477a157b78d3fee2fb4c15d32578108dc5d89866036c8",
- "sha256:70cebfe08fb32f83051971264466eadf183101e335d8107b80002e632f425511",
- "sha256:72cb7cf85e9df5251abd7b61a1af1fb77add15f40fa7328e924a9c0b6bc7a533",
- "sha256:7c55649965c35eb32c499d17dadfb8f53358b961582846e1bc06f66b9bccc556",
- "sha256:86b963a5de11336ec26bc4f839327673c9796b398b9f1fe6bb6150c2a5d00f0f",
- "sha256:8c73c9bcdfb526247c5e4f4f6cf581b9bb86b388df82cfcaffde0a6e7bf3b43a",
- "sha256:8e68c76c6aff4849089962d25346d6784d38e02baa23ffa513cf46be72e3a540",
- "sha256:97ac6b867a8f63debc64f44efdc695109d541ecc361ee2dce2c8884ab37360a1",
- "sha256:9d4f546af72aa001241d74a79caec278bcc007b4bcde4099994732e98012c858",
- "sha256:a28e69fe5468c9f5251c7e4e7232286d71b7dfadc74f312006ebe984433e9746",
- "sha256:fd509d4aa95404ce8d86b4e32ce66d5d706fd6646c205e1c2a715d87078683a2"
- ],
- "version": "==0.6.1"
+ "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"
},
"netaddr": {
"hashes": [
@@ -378,45 +361,45 @@
},
"oauthlib": {
"hashes": [
- "sha256:0ce32c5d989a1827e3f1148f98b9085ed2370fc939bf524c9c851d8714797298",
- "sha256:3e1e14f6cde7e5475128d30e97edc3bfb4dc857cb884d8714ec161fdbb3b358e"
+ "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
+ "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
],
- "version": "==3.0.1"
+ "version": "==3.1.0"
},
"oslo.config": {
"hashes": [
- "sha256:70fb3ad10f0efe6ff0c9c1c8c186ac8253c16faddd1f85cf80dca262a65ac896",
- "sha256:c9e1b3518c35925da4fef95a25bd1d42d1ec415f81d3668b384cf10e82ee6c36"
+ "sha256:32d21b3a70a1d356b03459667c3082c8cd4c0483ef98eb9e8f937b49ccb6df0f",
+ "sha256:b605addc284f7090167e5325de371a2437ba3e78d72c3ceb18091aa6e1744fc5"
],
- "version": "==6.9.0"
+ "version": "==8.0.1"
},
"oslo.i18n": {
"hashes": [
- "sha256:2669908357e1e49a754dc0c279512246341ae889176c568b89fd9233e65a6ae1",
- "sha256:7ecec04b682209292cf4dcfa29015956d15466af82352d92bfc442c8454f1ba2"
+ "sha256:2c6db1d4930ff49aaa7732d6d165fcf1454dc064fdab0a31144cb6d7d738bd61",
+ "sha256:b511fb702f25e48ff50eb5af3e600b5481b35ba405e16a098ee9d972949f59d8"
],
- "version": "==3.23.1"
+ "version": "==4.0.0"
},
"oslo.serialization": {
"hashes": [
- "sha256:5c7cd29c1015d3d00751cee76924923881796ba15639f949f0ff6eefdb00e185",
- "sha256:a7b177e42036643b3140414111054b6eb6510c32b30351a585948186de88a3bf"
+ "sha256:06612670c754aa77be1d42fa9301f5c8a633a1ae28c2f998788ba48fbb3284e5",
+ "sha256:c8d2de8788c4dd8900a5abacc1c29c1d36f3ead5eab392bc1e39a5dc193f78cf"
],
- "version": "==2.29.1"
+ "version": "==3.1.0"
},
"oslo.utils": {
"hashes": [
- "sha256:10cc04d6b75e57dd6e6eccd3745bbce6002567a77f8ddc4103bcea53763bca41",
- "sha256:3f6ed3da33ddd1648baa6cd20b1cf64ceb6c3cb3e1c745d63e793a7e5b1520c2"
+ "sha256:eb1f9627ffc9981ba0a4b93225669ef70d1a3114534af734748682bd5ed3ebcf",
+ "sha256:ebbbe0a3bdea22a2e5e0144e6b38d1afdebd2072850e478ff24bb75bd1a14784"
],
- "version": "==3.41.0"
+ "version": "==4.1.0"
},
"pbr": {
"hashes": [
- "sha256:0ce920b865091450bbcd452b35cf6d6eb8a6d9ce13ad2210d6e77557f85cf32b",
- "sha256:93d2dc6ee0c9af4dbc70bc1251d0e545a9910ca8863774761f92716dece400b6"
+ "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b",
+ "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"
],
- "version": "==5.2.1"
+ "version": "==5.4.4"
},
"prettytable": {
"hashes": [
@@ -428,40 +411,43 @@
},
"psycopg2": {
"hashes": [
- "sha256:00cfecb3f3db6eb76dcc763e71777da56d12b6d61db6a2c6ccbbb0bff5421f8f",
- "sha256:076501fc24ae13b2609ba2303d88d4db79072562f0b8cc87ec1667dedff99dc1",
- "sha256:4e2b34e4c0ddfeddf770d7df93e269700b080a4d2ec514fec668d71895f56782",
- "sha256:5cacf21b6f813c239f100ef78a4132056f93a5940219ec25d2ef833cbeb05588",
- "sha256:61f58e9ecb9e4dc7e30be56b562f8fc10ae3addcfcef51b588eed10a5a66100d",
- "sha256:8954ff6e47247bdd134db602fcadfc21662835bd92ce0760f3842eacfeb6e0f3",
- "sha256:b6e8c854cdc623028e558a409b06ea2f16d13438335941c7765d0a42b5bedd33",
- "sha256:baca21c0f7344576346e260454d0007313ccca8c170684707a63946b27a56c8f",
- "sha256:bb1735378770fb95dbe392d29e71405d45c8bdcfa064f916504833a92ab03c55",
- "sha256:de3d3c46c1ee18f996db42d1eb44cf1565cc9e38fb1dbd9b773ff6b3fa8035d7",
- "sha256:dee885602bb200bdcb1d30f6da6c7bb207360bc786d0a364fe1540dd14af0bab"
+ "sha256:4212ca404c4445dc5746c0d68db27d2cbfb87b523fe233dc84ecd24062e35677",
+ "sha256:47fc642bf6f427805daf52d6e52619fe0637648fe27017062d898f3bf891419d",
+ "sha256:72772181d9bad1fa349792a1e7384dde56742c14af2b9986013eb94a240f005b",
+ "sha256:8396be6e5ff844282d4d49b81631772f80dabae5658d432202faf101f5283b7c",
+ "sha256:893c11064b347b24ecdd277a094413e1954f8a4e8cdaf7ffbe7ca3db87c103f0",
+ "sha256:92a07dfd4d7c325dd177548c4134052d4842222833576c8391aab6f74038fc3f",
+ "sha256:965c4c93e33e6984d8031f74e51227bd755376a9df6993774fd5b6fb3288b1f4",
+ "sha256:9ab75e0b2820880ae24b7136c4d230383e07db014456a476d096591172569c38",
+ "sha256:b0845e3bdd4aa18dc2f9b6fb78fbd3d9d371ad167fd6d1b7ad01c0a6cdad4fc6",
+ "sha256:dca2d7203f0dfce8ea4b3efd668f8ea65cd2b35112638e488a4c12594015f67b",
+ "sha256:ed686e5926929887e2c7ae0a700e32c6129abb798b4ad2b846e933de21508151",
+ "sha256:ef6df7e14698e79c59c7ee7cf94cd62e5b869db369ed4b1b8f7b729ea825712a",
+ "sha256:f898e5cc0a662a9e12bde6f931263a1bbd350cfb18e1d5336a12927851825bb6"
],
"index": "pypi",
- "version": "==2.8.2"
+ "version": "==2.8.4"
},
"pyasn1": {
"hashes": [
- "sha256:da2420fe13a9452d8ae97a0e478adde1dee153b11ba832a95b223a2ba01c10f7",
- "sha256:da6b43a8c9ae93bc80e2739efb38cc776ba74a886e3e9318d65fe81a8b8a2c6e"
+ "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
+ "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
],
- "version": "==0.4.5"
+ "version": "==0.4.8"
},
"pyasn1-modules": {
"hashes": [
- "sha256:ef721f68f7951fab9b0404d42590f479e30d9005daccb1699b0a51bb4177db96",
- "sha256:f309b6c94724aeaf7ca583feb1cc70430e10d7551de5e36edfc1ae6909bcfb3c"
+ "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
+ "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"
],
- "version": "==0.2.5"
+ "version": "==0.2.8"
},
"pycparser": {
"hashes": [
- "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
+ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
+ "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
],
- "version": "==2.19"
+ "version": "==2.20"
},
"pyjwt": {
"hashes": [
@@ -472,31 +458,33 @@
},
"pyparsing": {
"hashes": [
- "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a",
- "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"
+ "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
+ "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
],
- "version": "==2.4.0"
+ "version": "==2.4.6"
},
- "pyrsistent": {
+ "python-dateutil": {
"hashes": [
- "sha256:16692ee739d42cf5e39cef8d27649a8c1fdb7aa99887098f1460057c5eb75c3a"
+ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
+ "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
- "version": "==0.15.2"
+ "version": "==2.8.1"
},
- "python-dateutil": {
+ "python-editor": {
"hashes": [
- "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
- "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
+ "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d",
+ "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b",
+ "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"
],
- "version": "==2.8.0"
+ "version": "==1.0.4"
},
"python-jose": {
"hashes": [
- "sha256:29701d998fe560e52f17246c3213a882a4a39da7e42c7015bcc1f7823ceaff1c",
- "sha256:ed7387f0f9af2ea0ddc441d83a6eb47a5909bd0c8a72ac3250e75afec2cc1371"
+ "sha256:1ac4caf4bfebd5a70cf5bd82702ed850db69b0b6e1d0ae7368e5f99ac01c9571",
+ "sha256:8484b7fdb6962e9d242cce7680469ecf92bda95d10bbcbbeb560cacdff3abfce"
],
"index": "pypi",
- "version": "==3.0.1"
+ "version": "==3.1.0"
},
"python-keystoneclient": {
"hashes": [
@@ -507,10 +495,10 @@
},
"pytz": {
"hashes": [
- "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda",
- "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"
+ "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
+ "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
],
- "version": "==2019.1"
+ "version": "==2019.3"
},
"pyyaml": {
"hashes": [
@@ -521,18 +509,18 @@
},
"requests": {
"hashes": [
- "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
- "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
+ "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
+ "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
],
"index": "pypi",
- "version": "==2.22.0"
+ "version": "==2.23.0"
},
"requests-oauthlib": {
"hashes": [
- "sha256:bd6533330e8748e94bf0b214775fed487d309b8b8fe823dc45641ebcd9a32f57",
- "sha256:d3ed0c8f2e3bbc6b344fa63d6f933745ab394469da38db16bdddb461c7e25140"
+ "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
+ "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
],
- "version": "==1.2.0"
+ "version": "==1.3.0"
},
"rfc3986": {
"hashes": [
@@ -550,50 +538,50 @@
},
"six": {
"hashes": [
- "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
- "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
+ "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
+ "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
],
- "version": "==1.12.0"
+ "version": "==1.14.0"
},
"sqlalchemy": {
"hashes": [
- "sha256:c7fef198b43ef31dfd783d094fd5ee435ce8717592e6784c45ba337254998017"
+ "sha256:c4cca4aed606297afbe90d4306b49ad3a4cd36feb3f87e4bfd655c57fd9ef445"
],
- "version": "==1.3.4"
+ "version": "==1.3.15"
},
"stevedore": {
"hashes": [
- "sha256:7be098ff53d87f23d798a7ce7ae5c31f094f3deb92ba18059b1aeb1ca9fec0a0",
- "sha256:7d1ce610a87d26f53c087da61f06f9b7f7e552efad2a7f6d2322632b5f932ea2"
+ "sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b",
+ "sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b"
],
- "version": "==1.30.1"
+ "version": "==1.32.0"
},
"urllib3": {
"hashes": [
- "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
- "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
+ "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
+ "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
],
- "version": "==1.25.3"
+ "version": "==1.25.8"
},
"websocket-client": {
"hashes": [
- "sha256:1151d5fb3a62dc129164292e1227655e4bbc5dd5340a5165dfae61128ec50aa9",
- "sha256:1fd5520878b68b84b5748bb30e592b10d0a91529d5383f74f4964e72b297fd3a"
+ "sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549",
+ "sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010"
],
- "version": "==0.56.0"
+ "version": "==0.57.0"
},
"werkzeug": {
"hashes": [
- "sha256:865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c",
- "sha256:a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6"
+ "sha256:1e0dedc2acb1f46827daa2e399c1485c8fa17c0d8e70b6b875b4e7f54bf408d2",
+ "sha256:b353856d37dec59d6511359f97f6a4b2468442e454bd1c98298ddce53cac1f04"
],
- "version": "==0.15.4"
+ "version": "==0.16.1"
},
"wrapt": {
"hashes": [
- "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533"
+ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"
],
- "version": "==1.11.1"
+ "version": "==1.12.1"
},
"xmltodict": {
"hashes": [
@@ -604,26 +592,19 @@
}
},
"develop": {
- "atomicwrites": {
- "hashes": [
- "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
- "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
- ],
- "version": "==1.3.0"
- },
"attrs": {
"hashes": [
- "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
- "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
+ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
+ "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
- "version": "==19.1.0"
+ "version": "==19.3.0"
},
"certifi": {
"hashes": [
- "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
- "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
+ "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
+ "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
],
- "version": "==2019.3.9"
+ "version": "==2019.11.28"
},
"chardet": {
"hashes": [
@@ -634,10 +615,10 @@
},
"click": {
"hashes": [
- "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
- "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
+ "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
+ "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
],
- "version": "==7.0"
+ "version": "==7.1.1"
},
"codacy-coverage": {
"hashes": [
@@ -649,78 +630,89 @@
},
"coverage": {
"hashes": [
- "sha256:0402b1822d513d0231589494bceddb067d20581f5083598c451b56c684b0e5d6",
- "sha256:0644e28e8aea9d9d563607ee8b7071b07dd57a4a3de11f8684cd33c51c0d1b93",
- "sha256:0874a283686803884ec0665018881130604956dbaa344f2539c46d82cbe29eda",
- "sha256:0988c3837df4bc371189bb3425d5232cf150055452034c232dda9cbe04f9c38e",
- "sha256:20bc3205b3100956bb72293fabb97f0ed972c81fed10b3251c90c70dcb0599ab",
- "sha256:2cc9142a3367e74eb6b19d58c53ebb1dfd7336b91cdcc91a6a2888bf8c7af984",
- "sha256:3ae9a0a59b058ce0761c3bd2c2d66ecb2ee2b8ac592620184370577f7a546fb3",
- "sha256:3b2e30b835df58cb973f478d09f3d82e90c98c8e5059acc245a8e4607e023801",
- "sha256:401e9b04894eb1498c639c6623ee78a646990ce5f095248e2440968aafd6e90e",
- "sha256:41ec5812d5decdaa72708be3018e7443e90def4b5a71294236a4df192cf9eab9",
- "sha256:475769b638a055e75b3d3219e054fe2a023c0b077ff15bff6c95aba7e93e6cac",
- "sha256:61424f4e2e82c4129a4ba71e10ebacb32a9ecd6f80de2cd05bdead6ba75ed736",
- "sha256:811969904d4dd0bee7d958898be8d9d75cef672d9b7e7db819dfeac3d20d2d0c",
- "sha256:86224bb99abfd672bf2f9fcecad5e8d7a3fa94f7f71513f2210460a0350307cd",
- "sha256:9a238a20a3af00665f8381f7e53e9c606f9bb652d2423f6b822f6cb790d887e8",
- "sha256:a23b3fbc14d4e6182ecebfd22f3729beef0636d151d94764a1c28330d185e4e5",
- "sha256:ac162b4ebe51b7a2b7f5e462c4402802633eb81e77c94f8a7c1ed8a556e72c75",
- "sha256:b6187378726c84365bf297b5dcdae8789b6a5823b200bea23797777e5a63be09",
- "sha256:bcd5723d905ed4a825f17410a53535f880b6d7548ae3d89078db7b1ceefcd853",
- "sha256:c48a4f9c5fb385269bb7fbaf9c1326a94863b65ec7f5c96b2ea56b252f01ad08",
- "sha256:cd40199d6f1c29c85b170d25589be9a97edff8ee7e62be180a2a137823896030",
- "sha256:d1bc331a7d069485ac1d8c25a0ea1f6aab6cb2a87146fb652222481c1bddc9ff",
- "sha256:d7e0cdc249aa0f94aa2e531b03999ddaf03a10b4fa090a894712d4c8066abd89",
- "sha256:e9ee8fcd8e067fcc5d7276d46e07e863102b70a52545ef4254df1ff0893ce75f",
- "sha256:eb313c23d983b7810504f42104e8dcd1c7ccdda8fbaab82aab92ab79fea19345",
- "sha256:f9cfd478654b509941b85ed70f870f5e3c74678f566bec12fd26545e5340ba47",
- "sha256:fae1fa144034d021a52cb9ea200eb8dedf91869c6df8202ad5d149b41ed91cc8"
- ],
- "version": "==5.0a5"
+ "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0",
+ "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30",
+ "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b",
+ "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0",
+ "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823",
+ "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe",
+ "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037",
+ "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6",
+ "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31",
+ "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd",
+ "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892",
+ "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1",
+ "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78",
+ "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac",
+ "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006",
+ "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014",
+ "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2",
+ "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7",
+ "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8",
+ "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7",
+ "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9",
+ "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1",
+ "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307",
+ "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a",
+ "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435",
+ "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0",
+ "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5",
+ "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441",
+ "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732",
+ "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de",
+ "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1"
+ ],
+ "version": "==5.0.4"
},
"deprecated": {
"hashes": [
- "sha256:2f293eb0eee34b1fcf3da530fe8fc4b0d71d43ddc2dc78e2ffb444b6c0868557",
- "sha256:749f6cdcfbdc3f79258f8154bad43fced95adc632c337675d0385959895894bc"
+ "sha256:408038ab5fdeca67554e8f6742d1521cd3cd0ee0ff9d47f29318a4f4da31c308",
+ "sha256:8b6a5aa50e482d8244a62e5582b96c372e87e3a28e8b49c316e46b95c76a611d"
],
- "version": "==1.2.5"
+ "version": "==1.2.7"
},
"flask": {
"hashes": [
- "sha256:ad7c6d841e64296b962296c2c2dabc6543752985727af86a975072dea984b6f3",
- "sha256:e7d32475d1de5facaa55e3958bc4ec66d3762076b074296aa50ef8fdc5b9df61"
+ "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
+ "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
],
"index": "pypi",
- "version": "==1.0.3"
+ "version": "==1.1.1"
+ },
+ "gitdb": {
+ "hashes": [
+ "sha256:284a6a4554f954d6e737cddcff946404393e030b76a282c6640df8efd6b3da5e",
+ "sha256:598e0096bb3175a0aab3a0b5aedaa18a9a25c6707e0eca0695ba1a0baf1b2150"
+ ],
+ "version": "==4.0.2"
},
"gitdb2": {
"hashes": [
- "sha256:83361131a1836661a155172932a13c08bda2db3674e4caa32368aa6eb02f38c2",
- "sha256:e3a0141c5f2a3f635c7209d56c496ebe1ad35da82fe4d3ec4aaa36278d70648a"
+ "sha256:0986cb4003de743f2b3aba4c828edd1ab58ce98e1c4a8acf72ef02760d4beb4e",
+ "sha256:a1c974e5fab8c2c90192c1367c81cbc54baec04244bda1816e9c8ab377d1cba3"
],
- "version": "==2.0.5"
+ "version": "==4.0.2"
},
"gitpython": {
"hashes": [
- "sha256:a313754737d9a2600b1267262769dda115566eec7e59b0ac855ff0cc9b1da81d",
- "sha256:e96f8e953cf9fee0a7599fc587667591328760b6341a0081ef311a942fc96204"
+ "sha256:5b5b7b29baa27680a7dff85f171a251d48ac37c5f04cba15d381b56991cc7a48"
],
- "version": "==2.1.1"
+ "version": "==3.0.6"
},
"idna": {
"hashes": [
- "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
- "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
+ "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
+ "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
],
- "version": "==2.8"
+ "version": "==2.9"
},
"importlib-metadata": {
"hashes": [
- "sha256:6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7",
- "sha256:cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db"
+ "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302",
+ "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"
],
- "version": "==0.18"
+ "markers": "python_version < '3.8'",
+ "version": "==1.5.0"
},
"itsdangerous": {
"hashes": [
@@ -731,10 +723,10 @@
},
"jinja2": {
"hashes": [
- "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
- "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
+ "sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484",
+ "sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668"
],
- "version": "==2.10.1"
+ "version": "==3.0.0a1"
},
"markupsafe": {
"hashes": [
@@ -742,13 +734,16 @@
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
+ "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
+ "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
+ "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
@@ -765,45 +760,62 @@
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
- "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
+ "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
+ "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
+ "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
"version": "==1.1.1"
},
+ "mock": {
+ "hashes": [
+ "sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6",
+ "sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb"
+ ],
+ "index": "pypi",
+ "version": "==1.3.0"
+ },
"more-itertools": {
"hashes": [
- "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7",
- "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a"
+ "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c",
+ "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"
],
- "markers": "python_version > '2.7'",
- "version": "==7.0.0"
+ "version": "==8.2.0"
},
"packaging": {
"hashes": [
- "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af",
- "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"
+ "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3",
+ "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"
],
- "version": "==19.0"
+ "version": "==20.3"
+ },
+ "pbr": {
+ "hashes": [
+ "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b",
+ "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"
+ ],
+ "version": "==5.4.4"
},
"pluggy": {
"hashes": [
- "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc",
- "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"
+ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
+ "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
- "version": "==0.12.0"
+ "version": "==0.13.1"
},
"py": {
"hashes": [
- "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
- "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
+ "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
+ "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
],
- "version": "==1.8.0"
+ "version": "==1.8.1"
},
"pygithub": {
"hashes": [
- "sha256:2ce91d4990efbfb7a0a1abd06e2106a3998a4a5810a4bdd3c1b6be5499918e9b"
+ "sha256:2638ea9a2070d995197dca2ac521c207f8de000cc3aa5e912e264932886781ba",
+ "sha256:f3e701a227a81a16fe35695ae812c1ce9290dfb6a5190b364c29cef7d8638a10"
],
"index": "pypi",
- "version": "==1.43.7"
+ "version": "==1.47"
},
"pyjwt": {
"hashes": [
@@ -814,34 +826,34 @@
},
"pyparsing": {
"hashes": [
- "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a",
- "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"
+ "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
+ "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
],
- "version": "==2.4.0"
+ "version": "==2.4.6"
},
"pytest": {
"hashes": [
- "sha256:4a784f1d4f2ef198fe9b7aef793e9fa1a3b2f84e822d9b3a64a181293a572d45",
- "sha256:926855726d8ae8371803f7b2e6ec0a69953d9c6311fa7c3b6c1b929ff92d27da"
+ "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172",
+ "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"
],
"index": "pypi",
- "version": "==4.6.3"
+ "version": "==5.4.1"
},
"pytest-cov": {
"hashes": [
- "sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6",
- "sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a"
+ "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b",
+ "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"
],
"index": "pypi",
- "version": "==2.7.1"
+ "version": "==2.8.1"
},
"pytest-flask": {
"hashes": [
- "sha256:283730b469604ecb94caac28df99a40b7c785b828dd8d3323596718b51dfaeb2",
- "sha256:d874781b622210d8c5d8061cdb091cb059fcb12203125110bd8e6f9256ccbf49"
+ "sha256:44948d3feab48c69e89b087129cc4db66bad9cb5aa472c08dfc798c69f4eac67",
+ "sha256:4d5678a045c07317618d80223ea124e21e8acc89dae109542dd1fdf6783d96c2"
],
"index": "pypi",
- "version": "==0.15.0"
+ "version": "==1.0.0"
},
"pyyaml": {
"hashes": [
@@ -852,33 +864,33 @@
},
"requests": {
"hashes": [
- "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
- "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
+ "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
+ "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
],
"index": "pypi",
- "version": "==2.22.0"
+ "version": "==2.23.0"
},
"six": {
"hashes": [
- "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
- "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
+ "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
+ "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
],
- "version": "==1.12.0"
+ "version": "==1.14.0"
},
- "smmap2": {
+ "smmap": {
"hashes": [
- "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde",
- "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a"
+ "sha256:171484fe62793e3626c8b05dd752eb2ca01854b0c55a1efc0dc4210fccb65446",
+ "sha256:5fead614cf2de17ee0707a8c6a5f2aa5a2fc6c698c70993ba42f515485ffda78"
],
- "version": "==2.0.5"
+ "version": "==3.0.1"
},
"trufflehog": {
"hashes": [
- "sha256:0453b0810b979e7fd1e9f35b6430989117782a9412b9b049ddd94358514257ae",
- "sha256:317c7fe67501cffaa40d53cdaf6a98d5c8ca1430e6881d33120796007cc4c4b5"
+ "sha256:53619f0c5be082abd377f987291ace80bc3b88f864972b1a30494780980f769e",
+ "sha256:ec3433f6ad2bc1894a050ecf5f58f3f1978dea23ff56557ce25bb26564bde286"
],
"index": "pypi",
- "version": "==2.0.99"
+ "version": "==2.1.11"
},
"trufflehogregexes": {
"hashes": [
@@ -889,37 +901,37 @@
},
"urllib3": {
"hashes": [
- "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
- "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
+ "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
+ "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
],
- "version": "==1.25.3"
+ "version": "==1.25.8"
},
"wcwidth": {
"hashes": [
- "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
- "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
+ "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603",
+ "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"
],
- "version": "==0.1.7"
+ "version": "==0.1.8"
},
"werkzeug": {
"hashes": [
- "sha256:865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c",
- "sha256:a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6"
+ "sha256:1e0dedc2acb1f46827daa2e399c1485c8fa17c0d8e70b6b875b4e7f54bf408d2",
+ "sha256:b353856d37dec59d6511359f97f6a4b2468442e454bd1c98298ddce53cac1f04"
],
- "version": "==0.15.4"
+ "version": "==0.16.1"
},
"wrapt": {
"hashes": [
- "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533"
+ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"
],
- "version": "==1.11.1"
+ "version": "==1.12.1"
},
"zipp": {
"hashes": [
- "sha256:8c1019c6aad13642199fbe458275ad6a84907634cc9f0989877ccc4a2840139d",
- "sha256:ca943a7e809cc12257001ccfb99e3563da9af99d52f261725e96dfe0f9275bc3"
+ "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
+ "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
],
- "version": "==0.5.1"
+ "version": "==3.1.0"
}
}
}
diff --git a/README.md b/README.md
index 99cf855..4ce0606 100644
--- a/README.md
+++ b/README.md
@@ -8,9 +8,78 @@ Each type of workspace environment should have a corresponding auth mechanism fo
OpenAPI Specification [here](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/uc-cdis/workspace-token-service/master/openapi/swagger.yaml).
-
## How a workspace interacts with WTS
- The workspace UI calls `/oauth2/authorization_url` to connect with Fence during user login, this will do an OIDC dance with fence to obtain a refresh token if it's a new user or if the user's previous refresh token is expired.
- The worker calls `/token?expires=seconds` to get an access token
+
+
+## Why isn't WTS part of Fence?
+
+The `/token` endpoint is [dependent on the local Kubernetes](https://github.com/uc-cdis/workspace-token-service/blob/master/wts/auth_plugins/k8s.py). It trusts the caller ([Gen3Fuse](https://github.com/uc-cdis/gen3-fuse)) to pass the correct user identity.
+
+
+
+
+## Gen3 Workspace architecture
+
+[![](docs/img/Export_to_WS_Architecture_Flow.png)](https://www.lucidchart.com/documents/edit/e844ca6b-fb75-460c-8a8e-5ddb4a17b8d9/0_0)
+
+
+## Configuration
+
+`dbcreds.json`:
+```
+{
+ "db_host": "xxx",
+ "db_username": "xxx",
+ "db_password": "xxx",
+ "db_database": "xxx"
+}
+```
+
+`appcreds.json`:
+
+```
+{
+ "wts_base_url": "https://my-data-commons.net/wts/",
+ "encryption_key": "xxx",
+ "secret_key": "xxx",
+
+ "fence_base_url": "https://my-data-commons.net/user/",
+ "oidc_client_id": "xxx",
+ "oidc_client_secret": "xxx",
+
+ "external_oidc": [
+ {
+ "base_url": "https://other-data-commons.net",
+ "oidc_client_id": "xxx",
+ "oidc_client_secret": "xxx",
+ "login_options": {
+ "other-google": {
+ "name": "Other Commons Google Login",
+ "params": {
+ "idp": "google"
+ }
+ },
+ "other-orcid": {
+ "name": "Other Commons ORCID Login",
+ "params": {
+ "idp": "fence",
+ "fence_idp": "orcid"
+ }
+ },
+ ...
+ }
+ },
+ ...
+ ]
+}
+```
+
+The default OIDC client configuration (`fence_base_url`, `oidc_client_id` and `oidc_client_secret`) is generated automatically during `gen3 kube-setup-wts`. Other clients can be created by running the following command in the external Fence: `fence-create client-create --client wts-my-data-commons --urls https://my-data-commons.net/wts/oauth2/authorize --username `, which returns a `(key id, secret key)` tuple. Any login option that is configured in the external Fence (the list is served at `https://other-data-commons.net/user/login`) can be configured here in the `login_options` section.
+
+Note that IDP IDs (`other-google` and `other-orcid` in the example above) must be unique _across the whole `external_oidc` block_.
+
+Also note that the OIDC clients you create must be granted `read-storage` access to all the data in the external Data Commons.
diff --git a/alembic.ini b/alembic.ini
new file mode 100644
index 0000000..6be3947
--- /dev/null
+++ b/alembic.ini
@@ -0,0 +1,83 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+script_location = migrations
+
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# timezone to use when rendering the date
+# within the migration file as well as the filename.
+# string value is passed to dateutil.tz.gettz()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the
+# "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; this defaults
+# to migrations/versions. When using multiple version
+# directories, initial revisions must be specified with --version-path
+# version_locations = %(here)s/bar %(here)s/bat migrations/versions
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts. See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks=black
+# black.type=console_scripts
+# black.entrypoint=black
+# black.options=-l 79
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/docs/img/Export_to_WS_Architecture_Flow.png b/docs/img/Export_to_WS_Architecture_Flow.png
new file mode 100644
index 0000000..fbd1dc8
Binary files /dev/null and b/docs/img/Export_to_WS_Architecture_Flow.png differ
diff --git a/docs/architecture.svg b/docs/img/architecture.svg
similarity index 100%
rename from docs/architecture.svg
rename to docs/img/architecture.svg
diff --git a/migrations/env.py b/migrations/env.py
new file mode 100644
index 0000000..0a1adf1
--- /dev/null
+++ b/migrations/env.py
@@ -0,0 +1,98 @@
+from alembic import context
+from logging.config import fileConfig
+import json
+from sqlalchemy import engine_from_config, pool
+from sqlalchemy.engine.url import URL
+
+from wts.models import db
+from wts.utils import get_config_var
+
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+
+target_metadata = db.metadata
+
+postgres_creds = get_config_var("POSTGRES_CREDS_FILE", "")
+if postgres_creds:
+ with open(postgres_creds, "r") as f:
+ creds = json.load(f)
+ try:
+ config.set_main_option(
+ "sqlalchemy.url",
+ str(
+ URL(
+ drivername="postgresql",
+ host=creds["db_host"],
+ port="5432",
+ username=creds["db_username"],
+ password=creds["db_password"],
+ database=creds["db_database"],
+ )
+ ),
+ )
+ except KeyError as e:
+ print("Postgres creds misconfiguration: {}".format(e))
+ exit(1)
+else:
+ url = get_config_var("SQLALCHEMY_DATABASE_URI")
+ if url:
+ config.set_main_option("sqlalchemy.url", url)
+ else:
+ print("Cannot find postgres creds location")
+ exit(1)
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url,
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ connectable = engine_from_config(
+ config.get_section(config.config_ini_section),
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
+
+ with connectable.connect() as connection:
+ context.configure(connection=connection, target_metadata=target_metadata)
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/migrations/script.py.mako b/migrations/script.py.mako
new file mode 100644
index 0000000..2c01563
--- /dev/null
+++ b/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/migrations/versions/27833deaf81f_add_idp.py b/migrations/versions/27833deaf81f_add_idp.py
new file mode 100644
index 0000000..1e251f4
--- /dev/null
+++ b/migrations/versions/27833deaf81f_add_idp.py
@@ -0,0 +1,26 @@
+"""Add IDP
+
+Revision ID: 27833deaf81f
+Revises: a38a346e6ded
+Create Date: 2020-03-15 19:38:26.321139
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "27833deaf81f"
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.add_column("refresh_token", sa.Column("idp", sa.VARCHAR))
+ op.execute("UPDATE refresh_token SET idp='default'")
+ op.alter_column("refresh_token", "idp", nullable=False)
+
+
+def downgrade():
+ op.drop_column("refresh_token", "idp")
diff --git a/openapi/swagger.yaml b/openapi/swagger.yaml
index a9e6932..874034c 100644
--- a/openapi/swagger.yaml
+++ b/openapi/swagger.yaml
@@ -25,6 +25,13 @@ paths:
summary: Check if user is connected and has a valid token
tags:
- auth
+ parameters:
+ - name: idp
+ type: string
+ in: query
+ description: unique ID of a configured IDP
+ required: false
+ default: 'default'
responses:
'200':
description: OK
@@ -40,6 +47,12 @@ paths:
in: query
name: redirect
type: string
+ - name: idp
+ type: string
+ in: query
+ description: unique ID of a configured IDP
+ required: false
+ default: 'default'
responses:
'302':
description: Redirect
@@ -48,6 +61,7 @@ paths:
/oauth2/authorize:
get:
summary: Send a token request to the OP
+ description: Will use the session-stored IDP parameter
tags:
- auth
responses:
@@ -75,6 +89,12 @@ paths:
in: query
name: expires
type: integer
+ - name: idp
+ type: string
+ in: query
+ description: unique ID of a configured IDP
+ required: false
+ default: 'default'
responses:
'200':
description: OK
@@ -85,4 +105,49 @@ paths:
type: string
'400':
description: User error
+ /external_oidc:
+ get:
+ summary: List the configured identity providers
+ description: >
+ List the configured identity providers and their configuration
+ details, including in how long the refresh token for the
+ currently logged in user will expire (or "null" if there is no refresh
+ token, or if it's already expired)
+ tags:
+ - auth
+ parameters:
+ - name: unexpired
+ in: query
+ type: boolean
+ description: Only return IDPs for which the currently logged in user has a valid refresh token.
+ required: false
+ default: 'false'
+ responses:
+ '200':
+ description: OK
+ schema:
+ type: object
+ properties:
+ providers:
+ type: array
+ items:
+ type: object
+ properties:
+ base_url:
+ type: string
+ idp:
+ type: string
+ name:
+ type: string
+ refresh_token_expiration:
+ type: string
+ urls:
+ type: array
+ items:
+ type: object
+ properties:
+ name:
+ type: string
+ url:
+ type: string
swagger: '2.0'
diff --git a/pull_request_template.md b/pull_request_template.md
deleted file mode 100644
index 4105e39..0000000
--- a/pull_request_template.md
+++ /dev/null
@@ -1,21 +0,0 @@
-Description about what this pull request does.
-
-Please make sure to follow the [DEV guidelines](https://gen3.org/resources/developer/dev-introduction/) before asking for review.
-
-### New Features
-- Implemented XXX
-
-### Breaking Changes
-
-
-### Bug Fixes
-
-
-### Improvements
-
-
-### Dependency updates
-
-
-### Deployment changes
-
diff --git a/tests/app_test.py b/tests/app_test.py
index 5871ed8..b83fbaa 100644
--- a/tests/app_test.py
+++ b/tests/app_test.py
@@ -1 +1,230 @@
-import pytest
+import json
+import mock
+import os
+import time
+import uuid
+
+from wts.models import RefreshToken
+from wts.resources.oauth2 import find_valid_refresh_token
+
+
+def insert_into_refresh_token_table(db_session, idp, data):
+ now = int(time.time())
+ db_session.add(
+ RefreshToken(
+ idp=idp,
+ token=data["refresh_token"],
+ username=data["username"],
+ userid=data["userid"],
+ expires=data.get("expires", now + 100),
+ jti=str(uuid.uuid4()),
+ )
+ )
+ db_session.commit()
+
+
+def create_logged_in_user_data(test_user, db_session):
+ now = int(time.time())
+ logged_in_user_data = {
+ "default": {
+ "username": test_user.username,
+ "userid": test_user.userid,
+ "refresh_token": "eyJhbGciOiJaaaa",
+ },
+ "idp_a": {
+ "username": test_user.username,
+ "userid": test_user.userid,
+ "refresh_token": "eyJhbGciOiJbbbb",
+ },
+ "idp_with_expired_token": {
+ "username": test_user.username,
+ "userid": test_user.userid,
+ "refresh_token": "eyJhbGciOiJcccc",
+ "expires": now - 100, # expired
+ },
+ }
+ for idp, data in logged_in_user_data.items():
+ insert_into_refresh_token_table(db_session, idp, data)
+ return logged_in_user_data
+
+
+def create_other_user_data(db_session):
+ other_user_data = {
+ "default": {
+ "username": "someone_else",
+ "userid": "123456",
+ "refresh_token": "eyJhbGciOiJzzzz",
+ },
+ "idp_a": {
+ "username": "someone_else",
+ "userid": "123456",
+ "refresh_token": "eyJhbGciOiJyyyy",
+ },
+ }
+ for idp, data in other_user_data.items():
+ insert_into_refresh_token_table(db_session, idp, data)
+ return other_user_data
+
+
+def test_find_valid_refresh_token(test_user, db_session):
+ logged_in_user_data = create_logged_in_user_data(test_user, db_session)
+
+ # valid refresh token
+ idp = "idp_a"
+ username = logged_in_user_data[idp]["username"]
+ assert find_valid_refresh_token(username, idp)
+
+ # expired refresh token
+ idp = "idp_with_expired_token"
+ assert not find_valid_refresh_token(username, idp)
+
+ # no existing refresh token for this idp
+ idp = "non_configured_idp"
+ assert not find_valid_refresh_token(username, idp)
+
+ # no existing refresh token for this user
+ idp = "idp_a"
+ username = "unknown_user"
+ assert not find_valid_refresh_token(username, idp)
+
+
+def test_connected_endpoint(client, test_user, db_session, auth_header):
+ res = client.get("/oauth2/connected", headers=auth_header)
+ assert res.status_code == 403
+
+ create_logged_in_user_data(test_user, db_session)
+
+ res = client.get("/oauth2/connected", headers=auth_header)
+ assert res.status_code == 200
+
+
+def test_token_endpoint(client, test_user, db_session):
+ logged_in_user_data = create_logged_in_user_data(test_user, db_session)
+ create_other_user_data(db_session)
+
+ # the token returned for a specific IDP should be created using the
+ # corresponding refresh_token, using the logged in user's username
+ res = client.get("/token/?idp=default")
+ assert res.status_code == 200
+ assert (
+ res.json["token"]
+ == "access_token_for_" + logged_in_user_data["default"]["refresh_token"]
+ )
+
+ res = client.get("/token/?idp=idp_a")
+ assert res.status_code == 200
+ assert (
+ res.json["token"]
+ == "access_token_for_" + logged_in_user_data["idp_a"]["refresh_token"]
+ )
+
+ # make sure the IDP we use is "default" when no IDP is requested
+ res = client.get("/token/")
+ assert res.status_code == 200
+ assert (
+ res.json["token"]
+ == "access_token_for_" + logged_in_user_data["default"]["refresh_token"]
+ )
+
+
+def test_authorize_endpoint(client, test_user, db_session, auth_header):
+ fake_tokens = {"default": "eyJhbGciOiJtttt", "idp_a": "eyJhbGciOiJuuuu"}
+
+ # mock `fetch_access_token` to avoid external calls
+ mocked_response = mock.MagicMock()
+ mocked_response.side_effect = [
+ # returned object for IDP "default":
+ {"refresh_token": fake_tokens["default"], "id_token": "eyJhbGciOiJ"},
+ # returned object for IDP "idp_a":
+ {"refresh_token": fake_tokens["idp_a"], "id_token": "eyJhbGciOiJ"},
+ ]
+ patched_fetch_access_token = mock.patch(
+ "authlib.client.OAuthClient.fetch_access_token", mocked_response
+ )
+ patched_fetch_access_token.start()
+
+ # mock `jwt.decode` to return fake data
+ now = int(time.time())
+ mocked_jwt_response = mock.MagicMock()
+ mocked_jwt_response.side_effect = [
+ # decoded id_token for IDP "default":
+ {"context": {"user": {"name": test_user.username}}},
+ # decoded refresh_token for IDP "default":
+ {"jti": str(uuid.uuid4()), "exp": now + 100, "sub": test_user.userid},
+ # decoded id_token for IDP "idp_a":
+ {"context": {"user": {"name": test_user.username}}},
+ # decoded refresh_token for IDP "idp_a":
+ {"jti": str(uuid.uuid4()), "exp": now + 100, "sub": test_user.userid},
+ ]
+ patched_jwt_decode = mock.patch("jose.jwt.decode", mocked_jwt_response)
+ patched_jwt_decode.start()
+
+ # get refresh token for IDP "default"
+ fake_state = "qwerty"
+ with client.session_transaction() as session:
+ session["state"] = fake_state
+ res = client.get(
+ "/oauth2/authorize?state={}".format(fake_state), headers=auth_header
+ )
+ assert res.status_code == 200, res.json
+
+ # get refresh token for IDP "idp_a"
+ with client.session_transaction() as session:
+ session["state"] = fake_state
+ session["idp"] = "idp_a"
+ res = client.get(
+ "/oauth2/authorize?state={}".format(fake_state), headers=auth_header
+ )
+ assert res.status_code == 200
+
+ # make sure the refresh tokens are in the DB
+ refresh_tokens = db_session.query(RefreshToken).all()
+ for t in refresh_tokens:
+ assert t.username == test_user.username
+ if t.idp == "default":
+ assert t.token == fake_tokens["default"]
+ else:
+ assert t.token == fake_tokens["idp_a"]
+
+
+def test_external_oidc_endpoint(client, test_user, db_session, auth_header):
+ with open(os.environ["SECRET_CONFIG"], "r") as f:
+ configured_oidc = json.load(f)["external_oidc"]
+ expected_oidc = {}
+ for provider in configured_oidc:
+ for idp, login_option in provider.get("login_options", {}).items():
+ expected_oidc[idp] = login_option
+ expected_oidc[idp]["base_url"] = provider["base_url"]
+ expected_oidc[idp]["oidc_client_id"] = provider["oidc_client_id"]
+
+ # GET /external_oidc before logging in
+ res = client.get("/external_oidc/", headers=auth_header)
+ assert res.status_code == 200
+ actual_oidc = res.json["providers"]
+
+ # the listed providers should be the configured providers
+ print("Configured providers: {}".format(expected_oidc))
+ print("Returned providers: {}".format(actual_oidc))
+ for provider in actual_oidc:
+ assert provider["idp"] in expected_oidc
+ data = expected_oidc[provider["idp"]]
+ assert provider["base_url"] == data["base_url"]
+ assert provider["name"] == data["name"]
+ assert provider["urls"][0]["url"].endswith(
+ "/oauth2/authorization_url?idp={}".format(provider["idp"])
+ )
+ assert provider["refresh_token_expiration"] == None
+
+ create_logged_in_user_data(test_user, db_session)
+
+ # GET /external_oidc after logging in
+ res = client.get("/external_oidc/", headers=auth_header)
+ assert res.status_code == 200
+ actual_oidc = res.json["providers"]
+ print("Returned providers after logging in: {}".format(actual_oidc))
+
+ for provider in actual_oidc:
+ if provider["idp"] == "idp_a": # test user is logged into this IDP
+ assert provider["refresh_token_expiration"] != None
+ else:
+ assert provider["refresh_token_expiration"] == None
diff --git a/tests/conftest.py b/tests/conftest.py
index 8aea96e..682f2bc 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,21 +1,32 @@
+from alembic.config import main as alembic_main
from cryptography.fernet import Fernet
+import jwt
+import mock
import pytest
+import requests
import os
+from sqlalchemy.exc import SQLAlchemyError
+import time
+import uuid
+
+from authutils.testing.fixtures import (
+ rsa_public_key,
+ _hazmat_rsa_private_key,
+ rsa_private_key,
+)
+
+from wts.auth_plugins import find_user
from wts.api import app as service_app
from wts.api import _setup
+from wts.models import db as _db
def test_settings():
+
settings = {
- "FENCE_BASE_URL": "localhost",
- "OIDC_CLIENT_ID": "test",
- "OIDC_CLIENT_SECRET": "test",
- "SECRET_KEY": "test",
- "SQLALCHEMY_DATABASE_URI": "postgresql://postgres:postgres@localhost:5432/wts_test",
- "WTS_BASE_URL": "/",
+ "SECRET_CONFIG": "tests/test_settings.json",
"ENCRYPTION_KEY": Fernet.generate_key().decode("utf-8"),
}
- print(settings)
for k, v in settings.items():
os.environ[k] = v
@@ -23,6 +34,147 @@ def test_settings():
@pytest.fixture(scope="session")
def app():
test_settings()
+ setup_test_database()
with service_app.app_context():
_setup(service_app)
return service_app
+
+
+def setup_test_database():
+ """
+ When running tests locally, we need to update the existing DB to
+ the latest version.
+ But in automated tests, a new DB is created from the latest models
+ so there is no need to migrate (and alembic fails when trying).
+ """
+ try:
+ alembic_main(["--raiseerr", "upgrade", "head"])
+ except SQLAlchemyError as e:
+ print("Skipping test DB migration: {}".format(e))
+
+
+@pytest.fixture(scope="function")
+def test_user():
+ return find_user()
+
+
+@pytest.fixture(scope="session")
+def db(app, request):
+ """Session-wide test database."""
+
+ def teardown():
+ _db.drop_all()
+
+ _db.app = app
+ _db.create_all()
+
+ request.addfinalizer(teardown)
+ return _db
+
+
+@pytest.fixture(scope="function")
+def db_session(db, request):
+ """Creates a new database session for a test."""
+ connection = db.engine.connect()
+ transaction = connection.begin()
+ options = dict(bind=connection, binds={})
+ session = db.create_scoped_session(options=options)
+ db.session = session
+
+ def teardown():
+ transaction.rollback()
+ connection.close()
+ session.remove()
+
+ request.addfinalizer(teardown)
+ return session
+
+
+@pytest.fixture(scope="session")
+def default_kid():
+ return "key-01"
+
+
+@pytest.fixture(scope="function")
+def auth_header(test_user, rsa_private_key, default_kid):
+ """
+ Return an authorization header containing the example JWT.
+
+ Args:
+ encoded_jwt (str): fixture
+
+ Return:
+ List[Tuple[str, str]]: the authorization header
+ """
+ now = int(time.time())
+ default_audiences = ["openid", "access", "user", "test_aud"]
+ claims = {
+ "pur": "access",
+ "aud": default_audiences,
+ "sub": test_user.userid,
+ "iss": "localhost",
+ "iat": now,
+ "exp": now + 600,
+ "jti": str(uuid.uuid4()),
+ "context": {"user": {"name": test_user.username, "projects": []}},
+ }
+ token_headers = {"kid": default_kid}
+ encoded_jwt = jwt.encode(
+ claims, headers=token_headers, key=rsa_private_key, algorithm="RS256"
+ )
+ encoded_jwt = encoded_jwt.decode("utf-8")
+ return [("Authorization", "Bearer {}".format(encoded_jwt))]
+
+
+@pytest.fixture(scope="function")
+def mock_requests(request, client, default_kid, rsa_public_key):
+ """
+ Mock GET requests for:
+ - obtaining JWT keys from Fence
+ Mock POST requests for:
+ - getting an access token from Fence using a refresh token
+ """
+
+ def do_patch():
+ def make_mock_get_response(*args, **kwargs):
+ mocked_response = mock.MagicMock(requests.Response)
+ request_url = args[0]
+ if request_url.endswith("/jwt/keys"):
+ mocked_response.status_code = 200
+ mocked_response.json = lambda: {"keys": [[default_kid, rsa_public_key]]}
+ return mocked_response
+ else:
+ client.get(request_url, args=args, kwargs=kwargs)
+
+ def make_mock_post_response(*args, **kwargs):
+ mocked_response = mock.MagicMock(requests.Response)
+ request_url = args[0]
+ request_params = kwargs["data"]
+
+ if request_url.endswith("/oauth2/token"):
+ mocked_response.status_code = 200
+ assert "refresh_token" in request_params
+ mocked_response.json = lambda: {
+ "access_token": "access_token_for_"
+ + request_params["refresh_token"]
+ }
+ return mocked_response
+ else:
+ client.post(request_url, args=args, kwargs=kwargs)
+
+ mocked_get_request = mock.MagicMock(side_effect=make_mock_get_response)
+ patched_get_request = mock.patch("requests.get", mocked_get_request)
+ patched_get_request.start()
+ request.addfinalizer(patched_get_request.stop)
+
+ mocked_post_request = mock.MagicMock(side_effect=make_mock_post_response)
+ patched_post_request = mock.patch("requests.post", mocked_post_request)
+ patched_post_request.start()
+ request.addfinalizer(patched_post_request.stop)
+
+ return do_patch
+
+
+@pytest.fixture(autouse=True)
+def mock_all_requests(mock_requests):
+ mock_requests()
diff --git a/tests/system_test.py b/tests/system_test.py
index 8d46415..5fd81e7 100644
--- a/tests/system_test.py
+++ b/tests/system_test.py
@@ -1,3 +1,3 @@
-def test_status_endpoint(client):
+def test_status_endpoint(client, db_session):
res = client.get("/_status")
assert res.status_code == 200
diff --git a/tests/test_settings.json b/tests/test_settings.json
new file mode 100644
index 0000000..7dd13ef
--- /dev/null
+++ b/tests/test_settings.json
@@ -0,0 +1,22 @@
+{
+ "fence_base_url": "localhost",
+ "oidc_client_id": "test",
+ "oidc_client_secret": "test",
+ "secret_key": "test",
+ "sqlalchemy_database_uri": "postgresql://postgres:postgres@localhost:5432/wts_test",
+ "wts_base_url": "/",
+ "external_oidc": [
+ {
+ "base_url": "localhost",
+ "oidc_client_id": "test2",
+ "oidc_client_secret": "test2",
+ "login_options": {
+ "idp_a": {"name": "IDP A", "params": {"idp": "google"}},
+ "idp_b": {
+ "name": "IDP B",
+ "params": {"idp": "fence", "fence_idp": "shibboleth"}
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/thog_config.json b/thog_config.json
new file mode 100644
index 0000000..98f667a
--- /dev/null
+++ b/thog_config.json
@@ -0,0 +1,8 @@
+{
+ "hardExcludes": [
+ "thog_config.json",
+ "truffles.json",
+ "Pipfile.lock",
+ "migrations/env.py"
+ ]
+}
\ No newline at end of file
diff --git a/wts/api.py b/wts/api.py
index c631dd8..990d34f 100644
--- a/wts/api.py
+++ b/wts/api.py
@@ -1,39 +1,22 @@
-# app.py
-
-import json
-import os
-
from authlib.client import OAuthClient
+from authlib.common.urls import add_params_to_uri
+from cryptography.fernet import Fernet
import flask
from flask import Flask
-from cryptography.fernet import Fernet
+import json
from cdislogging import get_logger
from cdiserrors import APIError
from .auth_plugins import setup_plugins
-from .blueprints import oauth2, tokens
+from .blueprints import oauth2, tokens, external_oidc
from .models import db, Base, RefreshToken
-
-app = Flask(__name__)
-app.logger = get_logger(__name__)
+from .utils import get_config_var as get_var
+from .version_data import VERSION, COMMIT
-def get_var(variable, default=None, secret_config={}):
- """
- get a secret from env var or mounted secret dir,
- raise exception if it doesn't exist
- """
- path = os.environ.get("SECRET_CONFIG")
- if not secret_config and path:
- with open(path, "r") as f:
- secret_config.update(json.load(f))
- value = secret_config.get(variable.lower(), os.environ.get(variable)) or default
- if value is None:
- raise Exception(
- "{} configuration is missing, abort initialization".format(variable)
- )
- return value
+app = Flask(__name__)
+app.logger = get_logger(__name__, log_level="info")
def load_settings(app):
@@ -41,12 +24,16 @@ def load_settings(app):
load setttings from environment variables
SECRET_KEY: app secret key to encrypt session cookies
ENCRYPTION_KEY: encryption key to encrypt credentials in database
- SQLALCHEMY_DATABASE_URI: database connection uri
+ POSTGRES_CREDS_FILE: JSON file with "db_username", "db_password",
+ "db_host" and "db_database" keys
+ SQLALCHEMY_DATABASE_URI: database connection uri. Overriden by
+ POSTGRES_CREDS_FILE
FENCE_BASE_URL: fence base url, eg: https://gen3_commons/user
WTS_BASE_URL: base url for this workspace token service
OIDC_CLIENT_ID: client id for the oidc client for this app
OIDC_CLIENT_SECRET: client secret for the oidc client for this app
AUTH_PLUGINS: a list of comma separate plugins, eg: k8s
+ EXTERNAL_OIDC: config for additional oidc handshakes
"""
app.secret_key = get_var("SECRET_KEY")
app.encrytion_key = Fernet(get_var("ENCRYPTION_KEY"))
@@ -61,7 +48,8 @@ def load_settings(app):
)
else:
app.config["SQLALCHEMY_DATABASE_URI"] = get_var("SQLALCHEMY_DATABASE_URI")
- fence_base_url = get_var("FENCE_BASE_URL")
+ url = get_var("FENCE_BASE_URL")
+ fence_base_url = url if url.endswith("/") else (url + "/")
plugins = get_var("AUTH_PLUGINS", "default")
plugins = set(plugins.split(","))
@@ -80,8 +68,29 @@ def load_settings(app):
"scope": "openid data user",
},
}
- app.config["OIDC"] = oauth_config
- app.config["OIDC_ISSUER"] = fence_base_url.strip("/")
+ app.config["OIDC"] = {"default": oauth_config}
+
+ for conf in get_var("EXTERNAL_OIDC", []):
+ url = get_var("BASE_URL", secret_config=conf)
+ fence_base_url = (url if url.endswith("/") else (url + "/")) + "user/"
+ for idp, idp_conf in conf.get("login_options", {}).items():
+ authorization_url = fence_base_url + "oauth2/authorize"
+ authorization_url = add_params_to_uri(
+ authorization_url, idp_conf.get("params", {})
+ )
+ app.config["OIDC"][idp] = {
+ "client_id": get_var("OIDC_CLIENT_ID", secret_config=conf),
+ "client_secret": get_var("OIDC_CLIENT_SECRET", secret_config=conf),
+ "api_base_url": fence_base_url,
+ "authorize_url": authorization_url,
+ "access_token_url": fence_base_url + "oauth2/token",
+ "refresh_token_url": fence_base_url + "oauth2/token",
+ "client_kwargs": {
+ "redirect_uri": wts_base_url + "oauth2/authorize",
+ "scope": "openid data user",
+ },
+ }
+
app.config["SESSION_COOKIE_NAME"] = "wts"
app.config["SESSION_COOKIE_SECURE"] = True
@@ -109,12 +118,16 @@ def setup():
def _setup(app):
load_settings(app)
- app.oauth2_client = OAuthClient(**app.config["OIDC"])
+ app.oauth2_clients = {
+ idp: OAuthClient(**conf) for idp, conf in app.config["OIDC"].items()
+ }
+ app.logger.info("Set up OIDC clients: {}".format(list(app.oauth2_clients.keys())))
setup_plugins(app)
db.init_app(app)
Base.metadata.create_all(bind=db.engine)
app.register_blueprint(oauth2.blueprint, url_prefix="/oauth2")
app.register_blueprint(tokens.blueprint, url_prefix="/token")
+ app.register_blueprint(external_oidc.blueprint, url_prefix="/external_oidc")
@app.route("/_status", methods=["GET"])
@@ -125,12 +138,28 @@ def health_check():
try:
db.session.query(RefreshToken).first()
return "Healthy", 200
- except:
+ except Exception as e:
+ app.logger.exception("Unable to query DB: {}".format(e))
return "Unhealthy", 500
+@app.route("/_version", methods=["GET"])
+def version():
+ """
+ Return the version of this service.
+ """
+
+ base = {"version": VERSION, "commit": COMMIT}
+
+ return flask.jsonify(base), 200
+
+
@app.route("/")
def root():
return flask.jsonify(
- {"/token": "get temporary token", "/oauth2": "oauth2 resources"}
+ {
+ "/token": "get temporary token",
+ "/oauth2": "oauth2 resources",
+ "/external_oidc": "list available identity providers",
+ }
)
diff --git a/wts/blueprints/external_oidc.py b/wts/blueprints/external_oidc.py
new file mode 100644
index 0000000..a0776c0
--- /dev/null
+++ b/wts/blueprints/external_oidc.py
@@ -0,0 +1,144 @@
+import flask
+import time
+
+from authutils.user import current_user
+
+from ..models import db, RefreshToken
+from ..utils import get_config_var, get_oauth_client
+
+
+blueprint = flask.Blueprint("external_oidc", __name__)
+
+blueprint.route("")
+
+external_oidc_cache = {}
+
+
+# this is called every 10 sec by the Gen3Fuse sidecar
+@blueprint.route("/", methods=["GET"])
+def get_external_oidc():
+ """
+ List the configured identity providers and their configuration
+ details, including the timestamp at which the refresh token for the
+ currently logged in user will expire (or "null" if there is no refresh
+ token, or if it's already expired). If "unexpired=true" is used, will
+ only return IDPs for which the currently logged in user has a valid
+ refresh token.
+
+ We use the "providers" field and make "urls" a list to match the format
+ of the Fence "/login" endpoint, and so that we can implement a more
+ complex "login options" logic in the future (automatically get the
+ available login options for each IDP, which could include dropdowns).
+ """
+
+ unexpired_only = flask.request.args.get("unexpired", "false").lower() == "true"
+
+ global external_oidc_cache
+ if not external_oidc_cache:
+ data = {
+ "providers": [
+ {
+ # name to display on the login button
+ "name": idp_conf["name"],
+ # unique ID of the configured identity provider
+ "idp": idp,
+ # hostname URL - gen3fuse uses it to get the manifests
+ "base_url": oidc_conf["base_url"],
+ # authorization URL to use for logging in
+ "urls": [
+ {
+ "name": idp_conf["name"],
+ "url": generate_authorization_url(idp),
+ }
+ ],
+ }
+ for oidc_conf in get_config_var("EXTERNAL_OIDC", [])
+ for idp, idp_conf in oidc_conf.get("login_options", {}).items()
+ ]
+ }
+ external_oidc_cache = data
+
+ # get the username of the current logged in user.
+ # `current_user` validates the token and relies on `OIDC_ISSUER`
+ # to know the issuer
+ client = get_oauth_client(idp="default")
+ flask.current_app.config["OIDC_ISSUER"] = client.api_base_url.strip("/")
+ username = None
+ try:
+ user = current_user
+ username = user.username
+ except Exception:
+ flask.current_app.logger.info(
+ "no logged in user: will return refresh_token_expiration=None for all IDPs"
+ )
+
+ # get all expirations at once (1 DB query)
+ idp_to_token_exp = get_refresh_token_expirations(
+ username, [p["idp"] for p in external_oidc_cache["providers"]]
+ )
+
+ result = {"providers": []}
+ for p in external_oidc_cache["providers"]:
+ # expiration of the current user's refresh token
+ exp = idp_to_token_exp[p["idp"]]
+ if exp or not unexpired_only:
+ p["refresh_token_expiration"] = exp
+ result["providers"].append(p)
+
+ return flask.jsonify(result), 200
+
+
+def generate_authorization_url(idp):
+ """
+ Args:
+ idp (string)
+
+ Returns:
+ str: authorization URL to go through the OIDC flow and get a
+ refresh token for this IDP
+ """
+ wts_base_url = get_config_var("WTS_BASE_URL")
+ authorization_url = wts_base_url + "oauth2/authorization_url?idp=" + idp
+ return authorization_url
+
+
+def seconds_to_human_time(seconds):
+ if seconds < 0:
+ return None
+ m, s = divmod(seconds, 60)
+ h, m = divmod(m, 60)
+ d, h = divmod(h, 24)
+ if d:
+ return "{} days".format(d)
+ if h:
+ return "{} hours".format(h)
+ if m:
+ return "{} minutes".format(m)
+ return "{} seconds".format(s)
+
+
+def get_refresh_token_expirations(username, idps):
+ """
+ Returns:
+ dict: IDP to expiration of the most recent refresh token, or None if it's expired.
+ """
+ now = int(time.time())
+ refresh_tokens = (
+ db.session.query(RefreshToken)
+ .filter_by(username=username)
+ .filter(RefreshToken.idp.in_(idps))
+ .order_by(RefreshToken.expires.asc())
+ )
+ if not refresh_tokens:
+ return {}
+ # the tokens are ordered by oldest to most recent, because we only want
+ # to return None if the most recent token is expired
+ expirations = {idp: None for idp in idps}
+ expirations.update(
+ {
+ t.idp: seconds_to_human_time(t.expires - now)
+ for t in refresh_tokens
+ if t.expires > now
+ }
+ )
+ return expirations
diff --git a/wts/blueprints/oauth2.py b/wts/blueprints/oauth2.py
index 3a55121..2dea8e8 100644
--- a/wts/blueprints/oauth2.py
+++ b/wts/blueprints/oauth2.py
@@ -1,9 +1,11 @@
-from cdiserrors import APIError, UserError, AuthNError, AuthZError
import flask
from urllib.parse import urljoin
-from ..resources import oauth2
-from ..auth import login_required
+
from authutils.user import current_user
+from cdiserrors import APIError, UserError, AuthNError, AuthZError
+
+from ..resources import oauth2
+from ..utils import get_oauth_client
blueprint = flask.Blueprint("oauth2", __name__)
@@ -14,14 +16,21 @@ def connected():
"""
Check if user is connected and has a valid token
"""
+ requested_idp = flask.request.args.get("idp", "default")
+
+ # `current_user` validates the token and relies on `OIDC_ISSUER`
+ # to know the issuer
+ client = get_oauth_client(idp=requested_idp)
+ flask.current_app.config["OIDC_ISSUER"] = client.api_base_url.strip("/")
+
try:
user = current_user
flask.current_app.logger.info(user)
username = user.username
- except:
+ except Exception:
flask.current_app.logger.exception("fail to get username")
raise AuthNError("user is not logged in")
- if oauth2.find_valid_refresh_token(username):
+ if oauth2.find_valid_refresh_token(username, requested_idp):
return "", 200
else:
raise AuthZError("user is not connected with token service or expired")
@@ -38,15 +47,15 @@ def get_authorization_url():
if redirect:
flask.session["redirect"] = redirect
+ requested_idp = flask.request.args.get("idp", "default")
+ client = get_oauth_client(idp=requested_idp)
# This will be the value that was put in the ``client_kwargs`` in config.
- redirect_uri = flask.current_app.oauth2_client.session.redirect_uri
+ redirect_uri = client.client_kwargs.get("redirect_uri")
# Get the authorization URL and the random state; save the state to check
# later, and return the URL.
- (
- authorization_url,
- state,
- ) = flask.current_app.oauth2_client.generate_authorize_redirect(redirect_uri)
+ (authorization_url, state) = client.generate_authorize_redirect(redirect_uri)
flask.session["state"] = state
+ flask.session["idp"] = requested_idp
return flask.redirect(authorization_url)
@@ -68,11 +77,17 @@ def logout_oauth():
"""
Log out the user.
To accomplish this, just revoke the refresh token if provided.
+
+ NOTE: this endpoint doesn't handle the "idp" parameter for now. If we want
+ to allow logging out, we'll have to revoke the token associated with the
+ specified IDP.
"""
url = urljoin(flask.current_app.config.get("USER_API"), "/oauth2/revoke")
token = flask.request.form.get("token")
+ client = get_oauth_client(idp="default")
+
try:
- flask.current_app.oauth2_client.session.revoke_token(url, token)
+ client.session.revoke_token(url, token)
except APIError as e:
msg = "could not log out, failed to revoke token: {}".format(e.message)
return msg, 400
diff --git a/wts/blueprints/tokens.py b/wts/blueprints/tokens.py
index 2b001e6..79fd46b 100644
--- a/wts/blueprints/tokens.py
+++ b/wts/blueprints/tokens.py
@@ -1,6 +1,5 @@
import flask
-
from ..auth import login_required
from ..tokens import get_access_token
diff --git a/wts/models.py b/wts/models.py
index c0b3966..ae9bbc9 100644
--- a/wts/models.py
+++ b/wts/models.py
@@ -16,3 +16,4 @@ class RefreshToken(Base):
username = Column(String)
userid = Column(String)
expires = Column(BigInteger)
+ idp = Column(String)
diff --git a/wts/resources/oauth2.py b/wts/resources/oauth2.py
index 74ade12..bd96fa1 100644
--- a/wts/resources/oauth2.py
+++ b/wts/resources/oauth2.py
@@ -1,15 +1,19 @@
-from authlib.client.errors import OAuthException
-from authlib.specs.rfc6749.errors import OAuth2Error
-from cdiserrors import AuthError
+from authlib.common.errors import AuthlibBaseError
from datetime import datetime
import flask
from jose import jwt
+from authutils.user import current_user
+from cdiserrors import AuthError
+
from ..models import RefreshToken, db
+from ..utils import get_oauth_client
def client_do_authorize():
- redirect_uri = flask.current_app.oauth2_client.session.redirect_uri
+ requested_idp = flask.session.get("idp", "default")
+ client = get_oauth_client(idp=requested_idp)
+ redirect_uri = client.client_kwargs.get("redirect_uri")
mismatched_state = (
"state" not in flask.request.args
or "state" not in flask.session
@@ -18,19 +22,19 @@ def client_do_authorize():
if mismatched_state:
raise AuthError("could not authorize; state did not match across auth requests")
try:
- tokens = flask.current_app.oauth2_client.fetch_access_token(
- redirect_uri, **flask.request.args.to_dict()
- )
- return refresh_refresh_token(tokens)
+ tokens = client.fetch_access_token(redirect_uri, **flask.request.args.to_dict())
+ return refresh_refresh_token(tokens, requested_idp)
except KeyError as e:
raise AuthError("error in token response: {}".format(tokens))
- except (OAuth2Error, OAuthException) as e:
+ except AuthlibBaseError as e:
raise AuthError(str(e))
-def find_valid_refresh_token(username):
+def find_valid_refresh_token(username, idp):
has_valid = False
- for token in db.session.query(RefreshToken).filter_by(username=username):
+ for token in (
+ db.session.query(RefreshToken).filter_by(username=username).filter_by(idp=idp)
+ ):
flask.current_app.logger.info("find token with exp {}".format(token.expires))
if datetime.fromtimestamp(token.expires) < datetime.now():
flask.current_app.logger.info("Purging expired token {}".format(token.jti))
@@ -39,7 +43,7 @@ def find_valid_refresh_token(username):
return has_valid
-def refresh_refresh_token(tokens):
+def refresh_refresh_token(tokens, idp):
"""
store new refresh token in db and purge all old tokens for the user
"""
@@ -61,7 +65,9 @@ def refresh_refresh_token(tokens):
id_token = jwt.decode(id_token, key=None, options=options)
content = jwt.decode(refresh_token, key=None, options=options)
userid = content["sub"]
- for old_token in db.session.query(RefreshToken).filter_by(userid=userid):
+ for old_token in (
+ db.session.query(RefreshToken).filter_by(userid=userid).filter_by(idp=idp)
+ ):
flask.current_app.logger.info(
"Refreshing token, purging {}".format(old_token.jti)
)
@@ -71,12 +77,26 @@ def refresh_refresh_token(tokens):
bytes(refresh_token, encoding="utf8")
)
+ # get the username of the current logged in user.
+ # `current_user` validates the token and relies on `OIDC_ISSUER`
+ # to know the issuer
+ client = get_oauth_client(idp="default")
+ flask.current_app.config["OIDC_ISSUER"] = client.api_base_url.strip("/")
+ user = current_user
+ username = user.username
+
+ flask.current_app.logger.info(
+ 'Linking username "{}" for IDP "{}" to current user "{}"'.format(
+ id_token["context"]["user"]["name"], idp, username
+ )
+ )
new_token = RefreshToken(
token=refresh_token,
userid=userid,
- username=id_token["context"]["user"]["name"],
+ username=username,
jti=content["jti"],
expires=content["exp"],
+ idp=idp,
)
db.session.add(new_token)
db.session.commit()
diff --git a/wts/tokens.py b/wts/tokens.py
index b39ef38..6bccf6d 100644
--- a/wts/tokens.py
+++ b/wts/tokens.py
@@ -1,17 +1,22 @@
+import flask
+import requests
import time
from cdiserrors import AuthError, InternalError
-import flask
-import requests
from .models import db, RefreshToken
+from .utils import get_oauth_client
def get_access_token(expires=None):
+ requested_idp = flask.request.args.get("idp", "default")
+ client = get_oauth_client(idp=requested_idp)
+
now = int(time.time())
refresh_token = (
db.session.query(RefreshToken)
.filter_by(username=flask.g.user.username)
+ .filter_by(idp=requested_idp)
.order_by(RefreshToken.expires.desc())
.first()
)
@@ -25,7 +30,6 @@ def get_access_token(expires=None):
token = flask.current_app.encryption_key.decrypt(token)
data = {"grant_type": "refresh_token", "refresh_token": token}
- client = flask.current_app.oauth2_client
auth = (client.client_id, client.client_secret)
try:
r = requests.post(client.access_token_url, data=data, auth=auth)
diff --git a/wts/utils.py b/wts/utils.py
new file mode 100644
index 0000000..755564b
--- /dev/null
+++ b/wts/utils.py
@@ -0,0 +1,41 @@
+import flask
+import json
+import os
+
+
+def get_config_var(variable, default=None, secret_config={}):
+ """
+ get a secret from env var or mounted secret dir,
+ raise exception if it doesn't exist
+ """
+ path = os.environ.get("SECRET_CONFIG")
+ if not secret_config and path:
+ with open(path, "r") as f:
+ secret_config.update(json.load(f))
+ value = secret_config.get(variable.lower(), os.environ.get(variable)) or default
+ if value is None:
+ raise Exception(
+ "{} configuration is missing, abort initialization".format(variable)
+ )
+ return value
+
+
+def get_oauth_client(idp=None):
+ """
+ Args:
+ idp (str, optional): IDP for the OAuthClient to return. Usually
+ the IDP argument of the current flask request. If not provided,
+ will return the default OAuthClient.
+
+ Returns:
+ (OAuthClient, str) tuple
+ """
+ idp = idp or "default"
+ try:
+ client = flask.current_app.oauth2_clients[idp]
+ except KeyError:
+ flask.current_app.logger.exception(
+ 'Requested IDP "{}" is not configured'.format(idp)
+ )
+ raise
+ return client
diff --git a/wts/version_data.py b/wts/version_data.py
new file mode 100644
index 0000000..7b419d2
--- /dev/null
+++ b/wts/version_data.py
@@ -0,0 +1,2 @@
+VERSION = ""
+COMMIT = ""