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 = ""