diff --git a/.pylintrc b/.pylintrc
index 5369262af..718b46dd4 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -1,6 +1,6 @@
[MASTER]
-disable=redefined-outer-name,arguments-differ,no-self-use,cyclic-import,fixme,locally-disabled,too-many-ancestors,too-few-public-methods,import-outside-toplevel,bad-continuation,signature-differs
+disable=arguments-differ,no-self-use,fixme,locally-disabled,too-many-ancestors,too-few-public-methods,import-outside-toplevel,bad-continuation,signature-differs,similarities,cyclic-import
load-plugins=pylint_django,pylint.extensions.bad_builtin
extension-pkg-whitelist=lxml
const-rgx=[a-zA-Z0-9_]{1,40}$
diff --git a/Pipfile b/Pipfile
index 3243d403a..f3c910f7e 100644
--- a/Pipfile
+++ b/Pipfile
@@ -13,8 +13,6 @@ django-dbbackup = "*"
django-filter = "*"
django-guardian = "*"
django-model-utils = "*"
-django-oauth-toolkit = "*"
-django-oidc-provider = "*"
django-otp = "*"
django-prometheus = "*"
django-recaptcha = "*"
@@ -23,13 +21,14 @@ django-rest-framework = "*"
django-storages = "*"
djangorestframework-guardian = "*"
drf-yasg = "*"
-kombu = "*"
+elastic-apm = "*"
+facebook-sdk = "*"
ldap3 = "*"
lxml = "*"
-oauthlib = "*"
packaging = "*"
psycopg2-binary = "*"
pycryptodome = "*"
+pyjwkest = "*"
pyuwsgi = "*"
pyyaml = "*"
qrcode = "*"
@@ -40,8 +39,6 @@ signxml = "*"
structlog = "*"
swagger-spec-validator = "*"
urllib3 = {extras = ["secure"],version = "*"}
-facebook-sdk = "*"
-elastic-apm = "*"
[requires]
python_version = "3.8"
@@ -49,16 +46,14 @@ python_version = "3.8"
[dev-packages]
autopep8 = "*"
bandit = "*"
+black = "==19.10b0"
bumpversion = "*"
colorama = "*"
coverage = "*"
django-debug-toolbar = "*"
+docker = "*"
pylint = "*"
pylint-django = "*"
-unittest-xml-reporting = "*"
-black = "*"
selenium = "*"
-docker = "*"
-
-[pipenv]
-allow_prereleases = true
+unittest-xml-reporting = "*"
+prospector = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index e86388061..6a1a545da 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "5c22d3a514247b663a07c6492cea09ab140346894a528db06bd805a4a3a4a320"
+ "sha256": "b7ba5405c03bf3526eebb29817887744a3e31bca019ad2e566ea23096c6a5cfe"
},
"pipfile-spec": 6,
"requires": {
@@ -46,18 +46,18 @@
},
"boto3": {
"hashes": [
- "sha256:640a8372ce0edfbb84a8f63584a0b64c78d61a751a27c2a47f92d2ebaf021ce4",
- "sha256:a6c9a3d3abbad2ff2e5751af599492a9271633a7c9fef343482524464c53e451"
+ "sha256:02ad765927bb46b9f45c3bce65e763960733919eee7883217995c5df5d096695",
+ "sha256:4421aad9a9740ce95199460f3262859e1c3594cc6c86cbe552745f4bbff34300"
],
"index": "pypi",
- "version": "==1.14.43"
+ "version": "==1.14.45"
},
"botocore": {
"hashes": [
- "sha256:1b46ffe1d13922066c873323186cbf97e77c137e08e27039d9d684552ccc4892",
- "sha256:1f6175bf59ffa068055b65f7d703eb1f748c338594a40dfdc645a6130280d8bb"
+ "sha256:17470c97435891cf40e147f533069de0109cda24c208c918f28997274bbac399",
+ "sha256:bc8b1c83ccc0d77963849b66a94bbb20a666ff0225aff84de7ed0175db1fd6f7"
],
- "version": "==1.17.44"
+ "version": "==1.17.45"
},
"celery": {
"hashes": [
@@ -207,21 +207,6 @@
"index": "pypi",
"version": "==4.0.0"
},
- "django-oauth-toolkit": {
- "hashes": [
- "sha256:28508f83385ab4313936ddedfb310eaa8a1dcb737153d2956383ce47e75c2fab",
- "sha256:d5a1044af9419ddc048390c5974777ea97874e5b78e33c609e17eebb8423afb2"
- ],
- "index": "pypi",
- "version": "==1.3.2"
- },
- "django-oidc-provider": {
- "hashes": [
- "sha256:3fa50d35ce614a68cde704606dbff86d8535a7679871ce3ec30100b28f3af50d"
- ],
- "index": "pypi",
- "version": "==0.7.0"
- },
"django-otp": {
"hashes": [
"sha256:0c9edbb3f4abc9ac6e43daf0a9e0e293e99ad917641cf8d7dbc49d613bcb5cd4",
@@ -232,11 +217,11 @@
},
"django-prometheus": {
"hashes": [
- "sha256:402228804b190be8bdbf20300ac3ae09043c7e3144e5dd648ddeb8f81a267f16",
- "sha256:57b97be6c88af9fc5b28a9fa8df629aab2b04f6a0f910c0669880d62f8ec3c0f"
+ "sha256:4c30aa8eb944fcf3cf10e20dfabbbe11ad5a84fce62abb3658feffa4e2ac2b97",
+ "sha256:8f25e86a3c310f40cf32cfa1b56a2b6df9cb2521e4cb794844958697d98fb3d1"
],
"index": "pypi",
- "version": "==2.1.0.dev61"
+ "version": "==2.0.0"
},
"django-recaptcha": {
"hashes": [
@@ -376,10 +361,10 @@
},
"jinja2": {
"hashes": [
- "sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484",
- "sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668"
+ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
+ "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
],
- "version": "==3.0.0a1"
+ "version": "==2.11.2"
},
"jmespath": {
"hashes": [
@@ -400,7 +385,6 @@
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
"sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
],
- "index": "pypi",
"version": "==4.6.11"
},
"ldap3": {
@@ -450,37 +434,47 @@
},
"markupsafe": {
"hashes": [
- "sha256:06358015a4dee8ee23ae426bf885616ab3963622defd829eb45b44e3dee3515f",
- "sha256:0b0c4fc852c5f02c6277ef3b33d23fcbe89b1b227460423e3335374da046b6db",
- "sha256:267677fc42afed5094fc5ea1c4236bbe4b6a00fe4b08e93451e65ae9048139c7",
- "sha256:303cb70893e2c345588fb5d5b86e0ca369f9bb56942f03064c5e3e75fa7a238a",
- "sha256:3c9b624a0d9ed5a5093ac4edc4e823e6b125441e60ef35d36e6f4a6fdacd5054",
- "sha256:42033e14cae1f6c86fc0c3e90d04d08ce73ac8e46ba420a0d22d545c2abd4977",
- "sha256:4e4a99b6af7bdc0856b50020c095848ec050356a001e1f751510aef6ab14d0e0",
- "sha256:4eb07faad54bb07427d848f31030a65a49ebb0cec0b30674f91cf1ddd456bfe4",
- "sha256:63a7161cd8c2bc563feeda45df62f42c860dd0675e2b8da2667f25bb3c95eaba",
- "sha256:68e0fd039b68d2945b4beb947d4023ca7f8e95b708031c345762efba214ea761",
- "sha256:8092a63397025c2f655acd42784b2a1528339b90b987beb9253f22e8cdbb36c3",
- "sha256:841218860683c0f2223e24756843d84cc49cccdae6765e04962607754a52d3e0",
- "sha256:94076b2314bd2f6cfae508ad65b4d493e3a58a50112b7a2cbb6287bdbc404ae8",
- "sha256:9d22aff1c5322e402adfb3ce40839a5056c353e711c033798cf4f02eb9f5124d",
- "sha256:b0e4584f62b3e5f5c1a7bcefd2b52f236505e6ef032cc508caa4f4c8dc8d3af1",
- "sha256:b1163ffc1384d242964426a8164da12dbcdbc0de18ea36e2c34b898ed38c3b45",
- "sha256:beac28ed60c8e838301226a7a85841d0af2068eba2dcb1a58c2d32d6c05e440e",
- "sha256:c29f096ce79c03054a1101d6e5fe6bf04b0bb489165d5e0e9653fb4fe8048ee1",
- "sha256:c58779966d53e5f14ba393d64e2402a7926601d1ac8adeb4e83893def79d0428",
- "sha256:cfe14b37908eaf7d5506302987228bff69e1b8e7071ccd4e70fd0283b1b47f0b",
- "sha256:e834249c45aa9837d0753351cdca61a4b8b383cc9ad0ff2325c97ff7b69e72a6",
- "sha256:eed1b234c4499811ee85bcefa22ef5e466e75d132502226ed29740d593316c1f"
+ "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",
+ "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
+ "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
+ "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
+ "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
+ "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
+ "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
+ "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
+ "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
+ "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
+ "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
+ "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
+ "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
+ "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
+ "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
+ "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
+ "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
- "version": "==2.0.0a1"
+ "version": "==1.1.1"
},
"oauthlib": {
"hashes": [
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
],
- "index": "pypi",
"version": "==3.1.0"
},
"packaging": {
@@ -630,6 +624,7 @@
"hashes": [
"sha256:5560fd5ba08655f29ff6ad1df1e15dc05abc9d976fcbcec8d2b5167f49b70222"
],
+ "index": "pypi",
"version": "==1.4.2"
},
"pyopenssl": {
@@ -641,10 +636,10 @@
},
"pyparsing": {
"hashes": [
- "sha256:1060635ca5ac864c2b7bc7b05a448df4e32d7d8c65e33cbe1514810d339672a2",
- "sha256:56a551039101858c9e189ac9e66e330a03fb7079e97ba6b50193643905f450ce"
+ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
+ "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
- "version": "==3.0.0a2"
+ "version": "==2.4.7"
},
"pyrsistent": {
"hashes": [
@@ -868,10 +863,10 @@
},
"astroid": {
"hashes": [
- "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703",
- "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"
+ "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1",
+ "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"
],
- "version": "==2.4.2"
+ "version": "==2.4.1"
},
"attrs": {
"hashes": [
@@ -925,39 +920,6 @@
],
"version": "==2020.6.20"
},
- "cffi": {
- "hashes": [
- "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e",
- "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c",
- "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e",
- "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1",
- "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4",
- "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2",
- "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c",
- "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0",
- "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798",
- "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1",
- "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4",
- "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731",
- "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4",
- "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c",
- "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487",
- "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e",
- "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f",
- "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123",
- "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c",
- "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b",
- "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650",
- "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad",
- "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75",
- "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82",
- "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7",
- "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15",
- "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa",
- "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281"
- ],
- "version": "==1.14.2"
- },
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
@@ -1020,30 +982,6 @@
"index": "pypi",
"version": "==5.2.1"
},
- "cryptography": {
- "hashes": [
- "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6",
- "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b",
- "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5",
- "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf",
- "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e",
- "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b",
- "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae",
- "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b",
- "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0",
- "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b",
- "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d",
- "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229",
- "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3",
- "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365",
- "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55",
- "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270",
- "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e",
- "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785",
- "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0"
- ],
- "version": "==2.9.2"
- },
"django": {
"hashes": [
"sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b",
@@ -1068,6 +1006,27 @@
"index": "pypi",
"version": "==4.3.0"
},
+ "dodgy": {
+ "hashes": [
+ "sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a",
+ "sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6"
+ ],
+ "version": "==0.2.1"
+ },
+ "flake8": {
+ "hashes": [
+ "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c",
+ "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"
+ ],
+ "version": "==3.8.3"
+ },
+ "flake8-polyfill": {
+ "hashes": [
+ "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9",
+ "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"
+ ],
+ "version": "==1.0.2"
+ },
"gitdb": {
"hashes": [
"sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac",
@@ -1143,6 +1102,20 @@
],
"version": "==5.4.5"
},
+ "pep8-naming": {
+ "hashes": [
+ "sha256:5d9f1056cb9427ce344e98d1a7f5665710e2f20f748438e308995852cfa24164",
+ "sha256:f3b4a5f9dd72b991bf7d8e2a341d2e1aa3a884a769b5aaac4f56825c1763bf3a"
+ ],
+ "version": "==0.10.0"
+ },
+ "prospector": {
+ "hashes": [
+ "sha256:43e5e187c027336b0e4c4aa6a82d66d3b923b5ec5b51968126132e32f9d14a2f"
+ ],
+ "index": "pypi",
+ "version": "==1.3.0"
+ },
"pycodestyle": {
"hashes": [
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
@@ -1150,28 +1123,47 @@
],
"version": "==2.6.0"
},
- "pycparser": {
+ "pydocstyle": {
"hashes": [
- "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
- "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
+ "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586",
+ "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"
],
- "version": "==2.20"
+ "version": "==5.0.2"
+ },
+ "pyflakes": {
+ "hashes": [
+ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
+ "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
+ ],
+ "version": "==2.2.0"
},
"pylint": {
"hashes": [
- "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc",
- "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c"
+ "sha256:b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c",
+ "sha256:dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b"
],
"index": "pypi",
- "version": "==2.5.3"
+ "version": "==2.5.2"
+ },
+ "pylint-celery": {
+ "hashes": [
+ "sha256:41e32094e7408d15c044178ea828dd524beedbdbe6f83f712c5e35bde1de4beb"
+ ],
+ "version": "==0.3"
},
"pylint-django": {
"hashes": [
- "sha256:770e0c55fb054c6378e1e8bb3fe22c7032a2c38ba1d1f454206ee9c6591822d7",
- "sha256:b8dcb6006ae9fa911810aba3bec047b9410b7d528f89d5aca2506b03c9235a49"
+ "sha256:06a64331c498a3f049ba669dc0c174b92209e164198d43e589b1096ee616d5f8",
+ "sha256:3d3436ba8d0fae576ae2db160e33a8f2746a101fda4463f2b3ff3a8b6fccec38"
],
"index": "pypi",
- "version": "==2.3.0"
+ "version": "==2.0.15"
+ },
+ "pylint-flask": {
+ "hashes": [
+ "sha256:f4d97de2216bf7bfce07c9c08b166e978fe9f2725de2a50a9845a97de7e31517"
+ ],
+ "version": "==0.6"
},
"pylint-plugin-utils": {
"hashes": [
@@ -1180,13 +1172,6 @@
],
"version": "==0.6"
},
- "pyopenssl": {
- "hashes": [
- "sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504",
- "sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507"
- ],
- "version": "==19.1.0"
- },
"pytz": {
"hashes": [
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
@@ -1244,13 +1229,25 @@
],
"version": "==2.24.0"
},
+ "requirements-detector": {
+ "hashes": [
+ "sha256:0d1e13e61ed243f9c3c86e6cbb19980bcb3a0e0619cde2ec1f3af70fdbee6f7b"
+ ],
+ "version": "==0.7"
+ },
"selenium": {
"hashes": [
- "sha256:5f5489a0c5fe2f09cc6bc3f32a0d53441ab36882c987269f2afe805979633ac1",
- "sha256:a9779ddc69cf03b75d94062c5e948f763919cf3341c77272f94cd05e6b4c7b32"
+ "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c",
+ "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"
],
"index": "pypi",
- "version": "==4.0.0a6.post2"
+ "version": "==3.141.0"
+ },
+ "setoptconf": {
+ "hashes": [
+ "sha256:5b0b5d8e0077713f5d5152d4f63be6f048d9a1bb66be15d089a11c898c3cf49c"
+ ],
+ "version": "==0.2.0"
},
"six": {
"hashes": [
@@ -1266,6 +1263,13 @@
],
"version": "==3.0.4"
},
+ "snowballstemmer": {
+ "hashes": [
+ "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0",
+ "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"
+ ],
+ "version": "==2.0.0"
+ },
"sqlparse": {
"hashes": [
"sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
diff --git a/e2e/test_provider_oauth.py b/e2e/test_provider_oauth2_github.py
similarity index 89%
rename from e2e/test_provider_oauth.py
rename to e2e/test_provider_oauth2_github.py
index 6c8f348f0..e5b2dde97 100644
--- a/e2e/test_provider_oauth.py
+++ b/e2e/test_provider_oauth2_github.py
@@ -1,7 +1,6 @@
"""test OAuth Provider flow"""
from time import sleep
-from oauth2_provider.generators import generate_client_id, generate_client_secret
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from structlog import get_logger
@@ -14,12 +13,16 @@ from passbook.core.models import Application
from passbook.flows.models import Flow
from passbook.policies.expression.models import ExpressionPolicy
from passbook.policies.models import PolicyBinding
-from passbook.providers.oauth.models import OAuth2Provider
+from passbook.providers.oauth2.generators import (
+ generate_client_id,
+ generate_client_secret,
+)
+from passbook.providers.oauth2.models import ClientTypes, OAuth2Provider, ResponseTypes
LOGGER = get_logger()
-class TestProviderOAuth(SeleniumTestCase):
+class TestProviderOAuth2Github(SeleniumTestCase):
"""test OAuth Provider flow"""
def setUp(self):
@@ -43,18 +46,18 @@ class TestProviderOAuth(SeleniumTestCase):
),
environment={
"GF_AUTH_GITHUB_ENABLED": "true",
- "GF_AUTH_GITHUB_allow_sign_up": "true",
+ "GF_AUTH_GITHUB_ALLOW_SIGN_UP": "true",
"GF_AUTH_GITHUB_CLIENT_ID": self.client_id,
"GF_AUTH_GITHUB_CLIENT_SECRET": self.client_secret,
"GF_AUTH_GITHUB_SCOPES": "user:email,read:org",
"GF_AUTH_GITHUB_AUTH_URL": self.url(
- "passbook_providers_oauth:github-authorize"
+ "passbook_providers_oauth2_github:github-authorize"
),
"GF_AUTH_GITHUB_TOKEN_URL": self.url(
- "passbook_providers_oauth:github-access-token"
+ "passbook_providers_oauth2_github:github-access-token"
),
"GF_AUTH_GITHUB_API_URL": self.url(
- "passbook_providers_oauth:github-user"
+ "passbook_providers_oauth2_github:github-user"
),
"GF_LOG_LEVEL": "debug",
},
@@ -80,12 +83,11 @@ class TestProviderOAuth(SeleniumTestCase):
)
provider = OAuth2Provider.objects.create(
name="grafana",
- client_type=OAuth2Provider.CLIENT_CONFIDENTIAL,
- authorization_grant_type=OAuth2Provider.GRANT_AUTHORIZATION_CODE,
client_id=self.client_id,
client_secret=self.client_secret,
+ client_type=ClientTypes.CONFIDENTIAL,
+ response_type=ResponseTypes.CODE,
redirect_uris="http://localhost:3000/login/github",
- skip_authorization=True,
authorization_flow=authorization_flow,
)
Application.objects.create(
@@ -134,12 +136,11 @@ class TestProviderOAuth(SeleniumTestCase):
)
provider = OAuth2Provider.objects.create(
name="grafana",
- client_type=OAuth2Provider.CLIENT_CONFIDENTIAL,
- authorization_grant_type=OAuth2Provider.GRANT_AUTHORIZATION_CODE,
client_id=self.client_id,
client_secret=self.client_secret,
+ client_type=ClientTypes.CONFIDENTIAL,
+ response_type=ResponseTypes.CODE,
redirect_uris="http://localhost:3000/login/github",
- skip_authorization=True,
authorization_flow=authorization_flow,
)
app = Application.objects.create(
@@ -161,7 +162,7 @@ class TestProviderOAuth(SeleniumTestCase):
).text,
)
self.assertEqual(
- "GitHub Compatibility: User Email",
+ "GitHub Compatibility: Access you Email addresses",
self.driver.find_element(
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]"
).text,
@@ -203,12 +204,11 @@ class TestProviderOAuth(SeleniumTestCase):
)
provider = OAuth2Provider.objects.create(
name="grafana",
- client_type=OAuth2Provider.CLIENT_CONFIDENTIAL,
- authorization_grant_type=OAuth2Provider.GRANT_AUTHORIZATION_CODE,
client_id=self.client_id,
client_secret=self.client_secret,
+ client_type=ClientTypes.CONFIDENTIAL,
+ response_type=ResponseTypes.CODE,
redirect_uris="http://localhost:3000/login/github",
- skip_authorization=True,
authorization_flow=authorization_flow,
)
app = Application.objects.create(
diff --git a/e2e/test_provider_oidc.py b/e2e/test_provider_oauth2_oidc.py
similarity index 73%
rename from e2e/test_provider_oidc.py
rename to e2e/test_provider_oauth2_oidc.py
index fa8ff6531..43f31c0ad 100644
--- a/e2e/test_provider_oidc.py
+++ b/e2e/test_provider_oauth2_oidc.py
@@ -1,9 +1,6 @@
-"""test OpenID Provider flow"""
+"""test OAuth2 OpenID Provider flow"""
from time import sleep
-from django.shortcuts import reverse
-from oauth2_provider.generators import generate_client_id, generate_client_secret
-from oidc_provider.models import Client, ResponseType
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
@@ -12,18 +9,33 @@ from structlog import get_logger
from docker import DockerClient, from_env
from docker.models.containers import Container
from docker.types import Healthcheck
-from e2e.utils import USER, SeleniumTestCase, ensure_rsa_key
+from e2e.utils import USER, SeleniumTestCase
from passbook.core.models import Application
+from passbook.crypto.models import CertificateKeyPair
from passbook.flows.models import Flow
from passbook.policies.expression.models import ExpressionPolicy
from passbook.policies.models import PolicyBinding
-from passbook.providers.oidc.models import OpenIDProvider
+from passbook.providers.oauth2.constants import (
+ SCOPE_OPENID,
+ SCOPE_OPENID_EMAIL,
+ SCOPE_OPENID_PROFILE,
+)
+from passbook.providers.oauth2.generators import (
+ generate_client_id,
+ generate_client_secret,
+)
+from passbook.providers.oauth2.models import (
+ ClientTypes,
+ OAuth2Provider,
+ ResponseTypes,
+ ScopeMapping,
+)
LOGGER = get_logger()
-class TestProviderOIDC(SeleniumTestCase):
- """test OpenID Provider flow"""
+class TestProviderOAuth2OIDC(SeleniumTestCase):
+ """test OAuth with OpenID Provider flow"""
def setUp(self):
self.client_id = generate_client_id()
@@ -50,13 +62,13 @@ class TestProviderOIDC(SeleniumTestCase):
"GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret,
"GF_AUTH_GENERIC_OAUTH_SCOPES": "openid email profile",
"GF_AUTH_GENERIC_OAUTH_AUTH_URL": (
- self.live_server_url + reverse("passbook_providers_oidc:authorize")
+ self.url("passbook_providers_oauth2:authorize")
),
"GF_AUTH_GENERIC_OAUTH_TOKEN_URL": (
- self.live_server_url + reverse("oidc_provider:token")
+ self.url("passbook_providers_oauth2:token")
),
"GF_AUTH_GENERIC_OAUTH_API_URL": (
- self.live_server_url + reverse("oidc_provider:userinfo")
+ self.url("passbook_providers_oauth2:userinfo")
),
"GF_LOG_LEVEL": "debug",
},
@@ -80,23 +92,22 @@ class TestProviderOIDC(SeleniumTestCase):
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
- client = Client.objects.create(
+ provider = OAuth2Provider.objects.create(
name="grafana",
- client_type="confidential",
+ client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
- _redirect_uris="http://localhost:3000/",
- _scope="openid userinfo",
+ rsa_key=CertificateKeyPair.objects.first(),
+ redirect_uris="http://localhost:3000/",
+ authorization_flow=authorization_flow,
+ response_type=ResponseTypes.CODE,
)
- # At least one of these objects must exist
- ensure_rsa_key()
- # This response_code object might exist or not, depending on the order the tests are run
- rp_type, _ = ResponseType.objects.get_or_create(value="code")
- client.response_types.set([rp_type])
- client.save()
- provider = OpenIDProvider.objects.create(
- oidc_client=client, authorization_flow=authorization_flow,
+ provider.property_mappings.set(
+ ScopeMapping.objects.filter(
+ scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
+ )
)
+ provider.save()
Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
)
@@ -121,25 +132,22 @@ class TestProviderOIDC(SeleniumTestCase):
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
- client = Client.objects.create(
+ provider = OAuth2Provider.objects.create(
name="grafana",
- client_type="confidential",
+ client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
- _redirect_uris="http://localhost:3000/login/generic_oauth",
- _scope="openid profile email",
- reuse_consent=False,
- require_consent=False,
+ rsa_key=CertificateKeyPair.objects.first(),
+ redirect_uris="http://localhost:3000/login/generic_oauth",
+ authorization_flow=authorization_flow,
+ response_type=ResponseTypes.CODE,
)
- # At least one of these objects must exist
- ensure_rsa_key()
- # This response_code object might exist or not, depending on the order the tests are run
- rp_type, _ = ResponseType.objects.get_or_create(value="code")
- client.response_types.set([rp_type])
- client.save()
- provider = OpenIDProvider.objects.create(
- oidc_client=client, authorization_flow=authorization_flow,
+ provider.property_mappings.set(
+ ScopeMapping.objects.filter(
+ scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
+ )
)
+ provider.save()
Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
)
@@ -182,25 +190,22 @@ class TestProviderOIDC(SeleniumTestCase):
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-explicit-consent"
)
- client = Client.objects.create(
+ provider = OAuth2Provider.objects.create(
name="grafana",
- client_type="confidential",
+ authorization_flow=authorization_flow,
+ response_type=ResponseTypes.CODE,
+ client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
- _redirect_uris="http://localhost:3000/login/generic_oauth",
- _scope="openid profile email",
- reuse_consent=False,
- require_consent=False,
+ rsa_key=CertificateKeyPair.objects.first(),
+ redirect_uris="http://localhost:3000/login/generic_oauth",
)
- # At least one of these objects must exist
- ensure_rsa_key()
- # This response_code object might exist or not, depending on the order the tests are run
- rp_type, _ = ResponseType.objects.get_or_create(value="code")
- client.response_types.set([rp_type])
- client.save()
- provider = OpenIDProvider.objects.create(
- oidc_client=client, authorization_flow=authorization_flow,
+ provider.property_mappings.set(
+ ScopeMapping.objects.filter(
+ scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
+ )
)
+ provider.save()
app = Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
)
@@ -261,25 +266,22 @@ class TestProviderOIDC(SeleniumTestCase):
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-explicit-consent"
)
- client = Client.objects.create(
+ provider = OAuth2Provider.objects.create(
name="grafana",
- client_type="confidential",
+ authorization_flow=authorization_flow,
+ response_type=ResponseTypes.CODE,
+ client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
- _redirect_uris="http://localhost:3000/login/generic_oauth",
- _scope="openid profile email",
- reuse_consent=False,
- require_consent=False,
+ rsa_key=CertificateKeyPair.objects.first(),
+ redirect_uris="http://localhost:3000/login/generic_oauth",
)
- # At least one of these objects must exist
- ensure_rsa_key()
- # This response_code object might exist or not, depending on the order the tests are run
- rp_type, _ = ResponseType.objects.get_or_create(value="code")
- client.response_types.set([rp_type])
- client.save()
- provider = OpenIDProvider.objects.create(
- oidc_client=client, authorization_flow=authorization_flow,
+ provider.property_mappings.set(
+ ScopeMapping.objects.filter(
+ scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
+ )
)
+ provider.save()
app = Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
)
diff --git a/e2e/test_sources_oauth.py b/e2e/test_sources_oauth.py
index 62f0255cc..01b29e056 100644
--- a/e2e/test_sources_oauth.py
+++ b/e2e/test_sources_oauth.py
@@ -2,7 +2,6 @@
from os.path import abspath
from time import sleep
-from oauth2_provider.generators import generate_client_secret
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
@@ -14,6 +13,7 @@ from docker.models.containers import Container
from docker.types import Healthcheck
from e2e.utils import SeleniumTestCase
from passbook.flows.models import Flow
+from passbook.providers.oauth2.generators import generate_client_secret
from passbook.sources.oauth.models import OAuthSource
TOKEN_URL = "http://127.0.0.1:5556/dex/token"
diff --git a/e2e/utils.py b/e2e/utils.py
index a9684f67c..a4db8dd3b 100644
--- a/e2e/utils.py
+++ b/e2e/utils.py
@@ -6,7 +6,6 @@ from inspect import getmembers, isfunction
from os import environ, makedirs
from time import time
-from Cryptodome.PublicKey import RSA
from django.apps import apps
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.db import connection, transaction
@@ -28,16 +27,6 @@ def USER() -> User: # noqa
return User.objects.get(username="pbadmin")
-def ensure_rsa_key():
- """Ensure that at least one RSAKey Object exists, create one if none exist"""
- from oidc_provider.models import RSAKey
-
- if not RSAKey.objects.exists():
- key = RSA.generate(2048)
- rsakey = RSAKey(key=key.exportKey("PEM").decode("utf8"))
- rsakey.save()
-
-
class SeleniumTestCase(StaticLiveServerTestCase):
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
diff --git a/passbook/api/urls.py b/passbook/api/urls.py
index a67986f8d..615334c52 100644
--- a/passbook/api/urls.py
+++ b/passbook/api/urls.py
@@ -1,10 +1,8 @@
"""passbook api urls"""
from django.urls import include, path
-from passbook.api.v1.urls import urlpatterns as v1_urls
from passbook.api.v2.urls import urlpatterns as v2_urls
urlpatterns = [
- path("v1/", include(v1_urls)),
path("v2beta/", include(v2_urls)),
]
diff --git a/passbook/api/v1/openid.py b/passbook/api/v1/openid.py
deleted file mode 100644
index df6b3181b..000000000
--- a/passbook/api/v1/openid.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""Passbook v1 OpenID API"""
-from django.http import JsonResponse
-from django.views import View
-from oauth2_provider.views.mixins import ScopedResourceMixin
-
-
-class OpenIDUserInfoView(ScopedResourceMixin, View):
- """Passbook v1 OpenID API"""
-
- required_scopes = ["openid:userinfo"]
-
- def get(self, request, *_, **__):
- """Passbook v1 OpenID API"""
- payload = {
- "sub": request.user.uuid.int,
- "name": request.user.get_full_name(),
- "given_name": request.user.name,
- "family_name": "",
- "preferred_username": request.user.username,
- "email": request.user.email,
- }
- return JsonResponse(payload)
diff --git a/passbook/api/v1/serializers.py b/passbook/api/v1/serializers.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/passbook/api/v1/urls.py b/passbook/api/v1/urls.py
deleted file mode 100644
index 14adc4305..000000000
--- a/passbook/api/v1/urls.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""Passbook API URLs"""
-from django.urls import path
-
-from passbook.api.v1.openid import OpenIDUserInfoView
-
-urlpatterns = [path("openid/", OpenIDUserInfoView.as_view(), name="openid")]
diff --git a/passbook/api/v2/urls.py b/passbook/api/v2/urls.py
index 3035c7bf9..1fa630d22 100644
--- a/passbook/api/v2/urls.py
+++ b/passbook/api/v2/urls.py
@@ -23,9 +23,8 @@ from passbook.policies.group_membership.api import GroupMembershipPolicyViewSet
from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet
from passbook.policies.password.api import PasswordPolicyViewSet
from passbook.policies.reputation.api import ReputationPolicyViewSet
-from passbook.providers.app_gw.api import ApplicationGatewayProviderViewSet
-from passbook.providers.oauth.api import OAuth2ProviderViewSet
-from passbook.providers.oidc.api import OpenIDProviderViewSet
+from passbook.providers.oauth2.api import OAuth2ProviderViewSet, ScopeMappingViewSet
+from passbook.providers.proxy.api import ProxyProviderViewSet
from passbook.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet
from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
from passbook.sources.oauth.api import OAuthSourceViewSet
@@ -70,14 +69,14 @@ router.register("policies/password", PasswordPolicyViewSet)
router.register("policies/reputation", ReputationPolicyViewSet)
router.register("providers/all", ProviderViewSet)
-router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet)
-router.register("providers/oauth", OAuth2ProviderViewSet)
-router.register("providers/openid", OpenIDProviderViewSet)
+router.register("providers/proxy", ProxyProviderViewSet)
+router.register("providers/oauth2", OAuth2ProviderViewSet)
router.register("providers/saml", SAMLProviderViewSet)
router.register("propertymappings/all", PropertyMappingViewSet)
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
+router.register("propertymappings/scope", ScopeMappingViewSet)
router.register("stages/all", StageViewSet)
router.register("stages/captcha", CaptchaStageViewSet)
diff --git a/passbook/core/admin.py b/passbook/core/admin.py
index d32203090..023981121 100644
--- a/passbook/core/admin.py
+++ b/passbook/core/admin.py
@@ -18,7 +18,7 @@ def admin_autoregister(app: AppConfig):
pass
-for app in apps.get_app_configs():
- if app.label.startswith("passbook_"):
- LOGGER.debug("Registering application for dj-admin", app=app.label)
- admin_autoregister(app)
+for _app in apps.get_app_configs():
+ if _app.label.startswith("passbook_"):
+ LOGGER.debug("Registering application for dj-admin", app=_app.label)
+ admin_autoregister(_app)
diff --git a/passbook/core/templates/error/generic.html b/passbook/core/templates/error/generic.html
index 2e61ea09e..30b7e9386 100644
--- a/passbook/core/templates/error/generic.html
+++ b/passbook/core/templates/error/generic.html
@@ -5,7 +5,7 @@
{% load passbook_utils %}
{% block title %}
-{% trans 'Bad Request' %}
+{% trans card_title %}
{% endblock %}
{% block card %}
diff --git a/passbook/crypto/models.py b/passbook/crypto/models.py
index abcbec7df..72c4e42e3 100644
--- a/passbook/crypto/models.py
+++ b/passbook/crypto/models.py
@@ -1,11 +1,12 @@
"""passbook crypto models"""
from binascii import hexlify
+from hashlib import md5
from typing import Optional
from uuid import uuid4
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
-from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
+from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import Certificate, load_pem_x509_certificate
from django.db import models
@@ -31,7 +32,8 @@ class CertificateKeyPair(CreatedUpdatedModel):
)
_cert: Optional[Certificate] = None
- _key: Optional[RSAPrivateKey] = None
+ _private_key: Optional[RSAPrivateKey] = None
+ _public_key: Optional[RSAPublicKey] = None
@property
def certificate(self) -> Certificate:
@@ -42,16 +44,23 @@ class CertificateKeyPair(CreatedUpdatedModel):
)
return self._cert
+ @property
+ def public_key(self) -> Optional[RSAPublicKey]:
+ """Get public key of the private key"""
+ if not self._public_key:
+ self._public_key = self.private_key.public_key()
+ return self._public_key
+
@property
def private_key(self) -> Optional[RSAPrivateKey]:
"""Get python cryptography PrivateKey instance"""
- if not self._key:
- self._key = load_pem_private_key(
+ if not self._private_key:
+ self._private_key = load_pem_private_key(
str.encode("\n".join([x.strip() for x in self.key_data.split("\n")])),
password=None,
backend=default_backend(),
)
- return self._key
+ return self._private_key
@property
def fingerprint(self) -> str:
@@ -60,6 +69,15 @@ class CertificateKeyPair(CreatedUpdatedModel):
"utf-8"
)
+ @property
+ def kid(self):
+ """Get Key ID used for JWKS"""
+ return "{0}".format(
+ md5(self.key_data.encode("utf-8")).hexdigest() # nosec
+ if self.key_data
+ else ""
+ )
+
def __str__(self) -> str:
return f"Certificate-Key Pair {self.name} {self.fingerprint}"
diff --git a/passbook/flows/models.py b/passbook/flows/models.py
index 06338e596..a4da0fb53 100644
--- a/passbook/flows/models.py
+++ b/passbook/flows/models.py
@@ -66,6 +66,8 @@ class Stage(models.Model):
return None
def __str__(self):
+ if hasattr(self, "__in_memory_type"):
+ return f"In-memory Stage {getattr(self, '__in_memory_type')}"
return f"Stage {self.name}"
diff --git a/passbook/flows/templates/flows/error.html b/passbook/flows/templates/flows/error.html
index 9f549077c..8b00ac92b 100644
--- a/passbook/flows/templates/flows/error.html
+++ b/passbook/flows/templates/flows/error.html
@@ -17,6 +17,6 @@
{% trans 'Something went wrong! Please try again later.' %}
{% if debug %}
-
{{ tb }}
+ {{ tb }}{{ error }}
{% endif %}
diff --git a/passbook/lib/templatetags/passbook_utils.py b/passbook/lib/templatetags/passbook_utils.py
index 6acca985b..2effddc1c 100644
--- a/passbook/lib/templatetags/passbook_utils.py
+++ b/passbook/lib/templatetags/passbook_utils.py
@@ -85,7 +85,6 @@ def verbose_name(obj) -> str:
if not obj:
return ""
if hasattr(obj, "verbose_name"):
- print(obj.verbose_name)
return obj.verbose_name
return obj._meta.verbose_name
diff --git a/passbook/lib/views.py b/passbook/lib/views.py
index c816b70fe..a82ec9e84 100644
--- a/passbook/lib/views.py
+++ b/passbook/lib/views.py
@@ -26,11 +26,13 @@ class CreateAssignPermView(CreateView):
return response
-def bad_request_message(request: HttpRequest, message: str) -> TemplateResponse:
+def bad_request_message(
+ request: HttpRequest, message: str, title="Bad Request"
+) -> TemplateResponse:
"""Return generic error page with message, with status code set to 400"""
return TemplateResponse(
request,
"error/generic.html",
- {"message": message, "card_title": _("Bad Request")},
+ {"message": message, "card_title": _(title)},
status=400,
)
diff --git a/passbook/policies/hibp/tests.py b/passbook/policies/hibp/tests.py
index 6c70256d1..0e79ac3fe 100644
--- a/passbook/policies/hibp/tests.py
+++ b/passbook/policies/hibp/tests.py
@@ -1,10 +1,10 @@
"""HIBP Policy tests"""
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
-from oauth2_provider.generators import generate_client_secret
from passbook.policies.hibp.models import HaveIBeenPwendPolicy
from passbook.policies.types import PolicyRequest, PolicyResult
+from passbook.providers.oauth2.generators import generate_client_secret
class TestHIBPPolicy(TestCase):
diff --git a/passbook/providers/app_gw/api.py b/passbook/providers/app_gw/api.py
deleted file mode 100644
index c4694f8b9..000000000
--- a/passbook/providers/app_gw/api.py
+++ /dev/null
@@ -1,45 +0,0 @@
-"""ApplicationGatewayProvider API Views"""
-from oauth2_provider.generators import generate_client_id, generate_client_secret
-from oidc_provider.models import Client
-from rest_framework.serializers import ModelSerializer
-from rest_framework.viewsets import ModelViewSet
-
-from passbook.providers.app_gw.models import ApplicationGatewayProvider
-from passbook.providers.oidc.api import OpenIDProviderSerializer
-
-
-class ApplicationGatewayProviderSerializer(ModelSerializer):
- """ApplicationGatewayProvider Serializer"""
-
- client = OpenIDProviderSerializer()
-
- def create(self, validated_data):
- instance = super().create(validated_data)
- instance.client = Client.objects.create(
- client_id=generate_client_id(), client_secret=generate_client_secret()
- )
- instance.save()
- return instance
-
- def update(self, instance, validated_data):
- self.instance.client.name = self.instance.name
- self.instance.client.redirect_uris = [
- f"http://{self.instance.host}/oauth2/callback",
- f"https://{self.instance.host}/oauth2/callback",
- ]
- self.instance.client.scope = ["openid", "email"]
- self.instance.client.save()
- return super().update(instance, validated_data)
-
- class Meta:
-
- model = ApplicationGatewayProvider
- fields = ["pk", "name", "internal_host", "external_host", "client"]
- read_only_fields = ["client"]
-
-
-class ApplicationGatewayProviderViewSet(ModelViewSet):
- """ApplicationGatewayProvider Viewset"""
-
- queryset = ApplicationGatewayProvider.objects.all()
- serializer_class = ApplicationGatewayProviderSerializer
diff --git a/passbook/providers/app_gw/apps.py b/passbook/providers/app_gw/apps.py
deleted file mode 100644
index a1702dcf9..000000000
--- a/passbook/providers/app_gw/apps.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""passbook Application Security Gateway app"""
-from django.apps import AppConfig
-
-
-class PassbookApplicationApplicationGatewayConfig(AppConfig):
- """passbook app_gw app"""
-
- name = "passbook.providers.app_gw"
- label = "passbook_providers_app_gw"
- verbose_name = "passbook Providers.Application Security Gateway"
- mountpoint = "application/gateway/"
diff --git a/passbook/providers/app_gw/forms.py b/passbook/providers/app_gw/forms.py
deleted file mode 100644
index a523d78d1..000000000
--- a/passbook/providers/app_gw/forms.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""passbook Application Security Gateway Forms"""
-from django import forms
-from oauth2_provider.generators import generate_client_id, generate_client_secret
-from oidc_provider.models import Client, ResponseType
-
-from passbook.providers.app_gw.models import ApplicationGatewayProvider
-
-
-class ApplicationGatewayProviderForm(forms.ModelForm):
- """Security Gateway Provider form"""
-
- def save(self, *args, **kwargs):
- if not self.instance.pk:
- # New instance, so we create a new OIDC client with random keys
- self.instance.client = Client.objects.create(
- client_id=generate_client_id(), client_secret=generate_client_secret()
- )
- self.instance.client.reuse_consent = False # This is managed by passbook
- self.instance.client.require_consent = False # This is managed by passbook
- self.instance.client.name = self.instance.name
- self.instance.client.response_types.set(
- [ResponseType.objects.get_by_natural_key("code")]
- )
- self.instance.client.redirect_uris = [
- f"{self.instance.external_host}/oauth2/callback",
- f"{self.instance.internal_host}/oauth2/callback",
- ]
- self.instance.client.scope = ["openid", "email", "profile"]
- self.instance.client.save()
- return super().save(*args, **kwargs)
-
- class Meta:
-
- model = ApplicationGatewayProvider
- fields = ["name", "authorization_flow", "internal_host", "external_host"]
- widgets = {
- "name": forms.TextInput(),
- "internal_host": forms.TextInput(),
- "external_host": forms.TextInput(),
- }
diff --git a/passbook/providers/app_gw/migrations/0001_initial.py b/passbook/providers/app_gw/migrations/0001_initial.py
deleted file mode 100644
index a5c6d746d..000000000
--- a/passbook/providers/app_gw/migrations/0001_initial.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Generated by Django 3.0.6 on 2020-05-19 22:08
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ("oidc_provider", "0026_client_multiple_response_types"),
- ("passbook_core", "0001_initial"),
- ]
-
- operations = [
- migrations.CreateModel(
- name="ApplicationGatewayProvider",
- fields=[
- (
- "provider_ptr",
- models.OneToOneField(
- auto_created=True,
- on_delete=django.db.models.deletion.CASCADE,
- parent_link=True,
- primary_key=True,
- serialize=False,
- to="passbook_core.Provider",
- ),
- ),
- ("name", models.TextField()),
- ("internal_host", models.TextField()),
- ("external_host", models.TextField()),
- (
- "client",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="oidc_provider.Client",
- ),
- ),
- ],
- options={
- "verbose_name": "Application Gateway Provider",
- "verbose_name_plural": "Application Gateway Providers",
- },
- bases=("passbook_core.provider",),
- ),
- ]
diff --git a/passbook/providers/app_gw/migrations/0002_auto_20200726_1745.py b/passbook/providers/app_gw/migrations/0002_auto_20200726_1745.py
deleted file mode 100644
index 09816b0dc..000000000
--- a/passbook/providers/app_gw/migrations/0002_auto_20200726_1745.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 3.0.8 on 2020-07-26 17:45
-
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("passbook_providers_app_gw", "0001_initial"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="applicationgatewayprovider",
- name="external_host",
- field=models.TextField(validators=[django.core.validators.URLValidator]),
- ),
- migrations.AlterField(
- model_name="applicationgatewayprovider",
- name="internal_host",
- field=models.TextField(validators=[django.core.validators.URLValidator]),
- ),
- ]
diff --git a/passbook/providers/app_gw/migrations/0003_auto_20200801_1752.py b/passbook/providers/app_gw/migrations/0003_auto_20200801_1752.py
deleted file mode 100644
index 60bf0431f..000000000
--- a/passbook/providers/app_gw/migrations/0003_auto_20200801_1752.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Generated by Django 3.0.8 on 2020-08-01 17:52
-
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("passbook_providers_app_gw", "0002_auto_20200726_1745"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="applicationgatewayprovider",
- name="external_host",
- field=models.TextField(
- validators=[
- django.core.validators.URLValidator(schemes=("http", "https"))
- ]
- ),
- ),
- migrations.AlterField(
- model_name="applicationgatewayprovider",
- name="internal_host",
- field=models.TextField(
- validators=[
- django.core.validators.URLValidator(schemes=("http", "https"))
- ]
- ),
- ),
- ]
diff --git a/passbook/providers/app_gw/models.py b/passbook/providers/app_gw/models.py
deleted file mode 100644
index 4c6836bf0..000000000
--- a/passbook/providers/app_gw/models.py
+++ /dev/null
@@ -1,50 +0,0 @@
-"""passbook app_gw models"""
-from typing import Optional, Type
-
-from django.core.validators import URLValidator
-from django.db import models
-from django.forms import ModelForm
-from django.http import HttpRequest
-from django.utils.translation import gettext as _
-from oidc_provider.models import Client
-
-from passbook.core.models import Provider
-from passbook.lib.utils.template import render_to_string
-
-
-class ApplicationGatewayProvider(Provider):
- """Protect applications that don't support any of the other
- Protocols by using a Reverse-Proxy."""
-
- name = models.TextField()
- internal_host = models.TextField(
- validators=[URLValidator(schemes=("http", "https"))]
- )
- external_host = models.TextField(
- validators=[URLValidator(schemes=("http", "https"))]
- )
-
- client = models.ForeignKey(Client, on_delete=models.CASCADE)
-
- def form(self) -> Type[ModelForm]:
- from passbook.providers.app_gw.forms import ApplicationGatewayProviderForm
-
- return ApplicationGatewayProviderForm
-
- def html_setup_urls(self, request: HttpRequest) -> Optional[str]:
- """return template and context modal with URLs for authorize, token, openid-config, etc"""
- from passbook.providers.app_gw.views import DockerComposeView
-
- docker_compose_yaml = DockerComposeView(request=request).get_compose(self)
- return render_to_string(
- "app_gw/setup_modal.html",
- {"provider": self, "docker_compose": docker_compose_yaml},
- )
-
- def __str__(self):
- return self.name
-
- class Meta:
-
- verbose_name = _("Application Gateway Provider")
- verbose_name_plural = _("Application Gateway Providers")
diff --git a/passbook/providers/oauth/api.py b/passbook/providers/oauth/api.py
deleted file mode 100644
index 9647b88d3..000000000
--- a/passbook/providers/oauth/api.py
+++ /dev/null
@@ -1,29 +0,0 @@
-"""OAuth2Provider API Views"""
-from rest_framework.serializers import ModelSerializer
-from rest_framework.viewsets import ModelViewSet
-
-from passbook.providers.oauth.models import OAuth2Provider
-
-
-class OAuth2ProviderSerializer(ModelSerializer):
- """OAuth2Provider Serializer"""
-
- class Meta:
-
- model = OAuth2Provider
- fields = [
- "pk",
- "name",
- "redirect_uris",
- "client_type",
- "authorization_grant_type",
- "client_id",
- "client_secret",
- ]
-
-
-class OAuth2ProviderViewSet(ModelViewSet):
- """OAuth2Provider Viewset"""
-
- queryset = OAuth2Provider.objects.all()
- serializer_class = OAuth2ProviderSerializer
diff --git a/passbook/providers/oauth/apps.py b/passbook/providers/oauth/apps.py
deleted file mode 100644
index b3f6ccdda..000000000
--- a/passbook/providers/oauth/apps.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""passbook auth oauth provider app config"""
-
-from django.apps import AppConfig
-
-
-class PassbookProviderOAuthConfig(AppConfig):
- """passbook auth oauth provider app config"""
-
- name = "passbook.providers.oauth"
- label = "passbook_providers_oauth"
- verbose_name = "passbook Providers.OAuth"
- mountpoint = ""
diff --git a/passbook/providers/oauth/forms.py b/passbook/providers/oauth/forms.py
deleted file mode 100644
index 8c470ef08..000000000
--- a/passbook/providers/oauth/forms.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""passbook OAuth2 Provider Forms"""
-
-from django import forms
-from django.utils.translation import gettext_lazy as _
-
-from passbook.flows.models import Flow, FlowDesignation
-from passbook.providers.oauth.models import OAuth2Provider
-
-
-class OAuth2ProviderForm(forms.ModelForm):
- """OAuth2 Provider form"""
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["authorization_flow"].queryset = Flow.objects.filter(
- designation=FlowDesignation.AUTHORIZATION
- )
-
- class Meta:
-
- model = OAuth2Provider
- fields = [
- "name",
- "authorization_flow",
- "redirect_uris",
- "client_type",
- "authorization_grant_type",
- "client_id",
- "client_secret",
- ]
- labels = {
- "client_id": _("Client ID"),
- "redirect_uris": _("Redirect URIs"),
- }
diff --git a/passbook/providers/oauth/migrations/0001_initial.py b/passbook/providers/oauth/migrations/0001_initial.py
deleted file mode 100644
index 7c3e7d1c3..000000000
--- a/passbook/providers/oauth/migrations/0001_initial.py
+++ /dev/null
@@ -1,104 +0,0 @@
-# Generated by Django 3.0.6 on 2020-05-19 22:08
-
-import django.db.models.deletion
-import oauth2_provider.generators
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- run_before = [
- ("oauth2_provider", "0001_initial"),
- ]
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ("passbook_core", "0001_initial"),
- ]
-
- operations = [
- migrations.CreateModel(
- name="OAuth2Provider",
- fields=[
- (
- "provider_ptr",
- models.OneToOneField(
- auto_created=True,
- on_delete=django.db.models.deletion.CASCADE,
- parent_link=True,
- primary_key=True,
- serialize=False,
- to="passbook_core.Provider",
- ),
- ),
- (
- "client_id",
- models.CharField(
- db_index=True,
- default=oauth2_provider.generators.generate_client_id,
- max_length=100,
- unique=True,
- ),
- ),
- (
- "redirect_uris",
- models.TextField(
- blank=True, help_text="Allowed URIs list, space separated"
- ),
- ),
- (
- "client_type",
- models.CharField(
- choices=[
- ("confidential", "Confidential"),
- ("public", "Public"),
- ],
- max_length=32,
- ),
- ),
- (
- "authorization_grant_type",
- models.CharField(
- choices=[
- ("authorization-code", "Authorization code"),
- ("implicit", "Implicit"),
- ("password", "Resource owner password-based"),
- ("client-credentials", "Client credentials"),
- ],
- max_length=32,
- ),
- ),
- (
- "client_secret",
- models.CharField(
- blank=True,
- db_index=True,
- default=oauth2_provider.generators.generate_client_secret,
- max_length=255,
- ),
- ),
- ("name", models.CharField(blank=True, max_length=255)),
- ("skip_authorization", models.BooleanField(default=False)),
- ("created", models.DateTimeField(auto_now_add=True)),
- ("updated", models.DateTimeField(auto_now=True)),
- (
- "user",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.CASCADE,
- related_name="passbook_providers_oauth_oauth2provider",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- ],
- options={
- "verbose_name": "OAuth2 Provider",
- "verbose_name_plural": "OAuth2 Providers",
- },
- bases=("passbook_core.provider", models.Model),
- ),
- ]
diff --git a/passbook/providers/oauth/models.py b/passbook/providers/oauth/models.py
deleted file mode 100644
index f1a9ad522..000000000
--- a/passbook/providers/oauth/models.py
+++ /dev/null
@@ -1,49 +0,0 @@
-"""Oauth2 provider product extension"""
-
-from typing import Optional, Type
-
-from django.forms import ModelForm
-from django.http import HttpRequest
-from django.shortcuts import reverse
-from django.utils.translation import gettext as _
-from oauth2_provider.models import AbstractApplication
-
-from passbook.core.models import Provider
-from passbook.lib.utils.template import render_to_string
-
-
-class OAuth2Provider(Provider, AbstractApplication):
- """Generic OAuth2 Provider for applications not using OpenID-Connect.
- This Provider also supports the GitHub-pretend mode for Applications that don't support
- generic OAuth."""
-
- def form(self) -> Type[ModelForm]:
- from passbook.providers.oauth.forms import OAuth2ProviderForm
-
- return OAuth2ProviderForm
-
- def __str__(self):
- return self.name
-
- def html_setup_urls(self, request: HttpRequest) -> Optional[str]:
- """return template and context modal with URLs for authorize, token, openid-config, etc"""
- return render_to_string(
- "providers/oauth/setup_url_modal.html",
- {
- "provider": self,
- "authorize_url": request.build_absolute_uri(
- reverse("passbook_providers_oauth:oauth2-authorize")
- ),
- "token_url": request.build_absolute_uri(
- reverse("passbook_providers_oauth:token")
- ),
- "userinfo_url": request.build_absolute_uri(
- reverse("passbook_api:openid")
- ),
- },
- )
-
- class Meta:
-
- verbose_name = _("OAuth2 Provider")
- verbose_name_plural = _("OAuth2 Providers")
diff --git a/passbook/providers/oauth/settings.py b/passbook/providers/oauth/settings.py
deleted file mode 100644
index 925787a30..000000000
--- a/passbook/providers/oauth/settings.py
+++ /dev/null
@@ -1,31 +0,0 @@
-"""passbook OAuth_Provider"""
-from django.conf import settings
-
-CORS_ORIGIN_ALLOW_ALL = settings.DEBUG
-
-REQUEST_APPROVAL_PROMPT = "auto"
-
-INSTALLED_APPS = [
- "oauth2_provider",
- "corsheaders",
-]
-MIDDLEWARE = [
- "oauth2_provider.middleware.OAuth2TokenMiddleware",
- "corsheaders.middleware.CorsMiddleware",
-]
-AUTHENTICATION_BACKENDS = [
- "oauth2_provider.backends.OAuth2Backend",
-]
-
-OAUTH2_PROVIDER_APPLICATION_MODEL = "passbook_providers_oauth.OAuth2Provider"
-
-OAUTH2_PROVIDER = {
- # this is the list of available scopes
- "SCOPES": {
- "openid": "Access OpenID Userinfo",
- "userinfo": "Access OpenID Userinfo",
- "email": "Access OpenID Email",
- "user:email": "GitHub Compatibility: User Email",
- "read:org": "GitHub Compatibility: User Groups",
- }
-}
diff --git a/passbook/providers/oauth/templates/providers/oauth/setup_url_modal.html b/passbook/providers/oauth/templates/providers/oauth/setup_url_modal.html
deleted file mode 100644
index 26419ba45..000000000
--- a/passbook/providers/oauth/templates/providers/oauth/setup_url_modal.html
+++ /dev/null
@@ -1,40 +0,0 @@
-{% load i18n %}
-
-
-
diff --git a/passbook/providers/oauth/urls.py b/passbook/providers/oauth/urls.py
deleted file mode 100644
index 4da8b3a07..000000000
--- a/passbook/providers/oauth/urls.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""passbook oauth_provider urls"""
-
-from django.urls import include, path
-from oauth2_provider import views
-
-from passbook.providers.oauth.views import github, oauth2
-
-oauth_urlpatterns = [
- # Custom OAuth2 Authorize View
- path(
- "authorize/",
- oauth2.AuthorizationFlowInitView.as_view(),
- name="oauth2-authorize",
- ),
- # OAuth API
- path("token/", views.TokenView.as_view(), name="token"),
- path("revoke_token/", views.RevokeTokenView.as_view(), name="revoke-token"),
- path("introspect/", views.IntrospectTokenView.as_view(), name="introspect"),
-]
-
-github_urlpatterns = [
- path(
- "login/oauth/authorize",
- oauth2.AuthorizationFlowInitView.as_view(),
- name="github-authorize",
- ),
- path(
- "login/oauth/access_token",
- views.TokenView.as_view(),
- name="github-access-token",
- ),
- path("user", github.GitHubUserView.as_view(), name="github-user"),
- path("user/teams", github.GitHubUserTeamsView.as_view(), name="github-user-teams"),
-]
-
-urlpatterns = [
- path("", include(github_urlpatterns)),
- path("application/oauth/", include(oauth_urlpatterns)),
-]
diff --git a/passbook/providers/oauth/views/__init__.py b/passbook/providers/oauth/views/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/passbook/providers/oauth/views/oauth2.py b/passbook/providers/oauth/views/oauth2.py
deleted file mode 100644
index 7186c2dc1..000000000
--- a/passbook/providers/oauth/views/oauth2.py
+++ /dev/null
@@ -1,136 +0,0 @@
-"""passbook OAuth2 Views"""
-from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
-from django.shortcuts import get_object_or_404
-from django.views import View
-from oauth2_provider.exceptions import OAuthToolkitError
-from oauth2_provider.scopes import get_scopes_backend
-from oauth2_provider.views.base import AuthorizationView
-from structlog import get_logger
-
-from passbook.audit.models import Event, EventAction
-from passbook.core.models import Application
-from passbook.flows.models import in_memory_stage
-from passbook.flows.planner import (
- PLAN_CONTEXT_APPLICATION,
- PLAN_CONTEXT_SSO,
- FlowPlanner,
-)
-from passbook.flows.stage import StageView
-from passbook.flows.views import SESSION_KEY_PLAN
-from passbook.lib.utils.urls import redirect_with_qs
-from passbook.policies.mixins import PolicyAccessMixin
-from passbook.providers.oauth.models import OAuth2Provider
-from passbook.stages.consent.stage import PLAN_CONTEXT_CONSENT_TEMPLATE
-
-LOGGER = get_logger()
-
-PLAN_CONTEXT_CLIENT_ID = "client_id"
-PLAN_CONTEXT_REDIRECT_URI = "redirect_uri"
-PLAN_CONTEXT_RESPONSE_TYPE = "response_type"
-PLAN_CONTEXT_STATE = "state"
-
-PLAN_CONTEXT_CODE_CHALLENGE = "code_challenge"
-PLAN_CONTEXT_CODE_CHALLENGE_METHOD = "code_challenge_method"
-PLAN_CONTEXT_SCOPE = "scope"
-PLAN_CONTEXT_NONCE = "nonce"
-PLAN_CONTEXT_SCOPE_DESCRIPTION = "scope_descriptions"
-
-
-class AuthorizationFlowInitView(PolicyAccessMixin, View):
- """OAuth2 Flow initializer, checks access to application and starts flow"""
-
- # pylint: disable=unused-argument
- def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
- """Check access to application, start FlowPLanner, return to flow executor shell"""
- client_id = request.GET.get("client_id")
- provider = get_object_or_404(OAuth2Provider, client_id=client_id)
- try:
- application = self.provider_to_application(provider)
- except Application.DoesNotExist:
- return self.handle_no_permission_authorized()
- # Check if user is unauthenticated, so we pass the application
- # for the identification stage
- if not request.user.is_authenticated:
- return self.handle_no_permission(application)
- # Check permissions
- result = self.user_has_access(application)
- if not result.passing:
- return self.handle_no_permission_authorized()
- # Regardless, we start the planner and return to it
- planner = FlowPlanner(provider.authorization_flow)
- planner.allow_empty_flows = True
- # Save scope descriptions
- scopes = request.GET.get(PLAN_CONTEXT_SCOPE)
- all_scopes = get_scopes_backend().get_all_scopes()
-
- plan = planner.plan(
- self.request,
- {
- PLAN_CONTEXT_SSO: True,
- PLAN_CONTEXT_APPLICATION: application,
- PLAN_CONTEXT_CLIENT_ID: client_id,
- PLAN_CONTEXT_REDIRECT_URI: request.GET.get(PLAN_CONTEXT_REDIRECT_URI),
- PLAN_CONTEXT_RESPONSE_TYPE: request.GET.get(PLAN_CONTEXT_RESPONSE_TYPE),
- PLAN_CONTEXT_STATE: request.GET.get(PLAN_CONTEXT_STATE),
- PLAN_CONTEXT_SCOPE: scopes,
- PLAN_CONTEXT_NONCE: request.GET.get(PLAN_CONTEXT_NONCE),
- PLAN_CONTEXT_SCOPE_DESCRIPTION: [
- all_scopes[scope] for scope in scopes.split(" ")
- ],
- PLAN_CONTEXT_CONSENT_TEMPLATE: "providers/oauth/consent.html",
- },
- )
-
- plan.append(in_memory_stage(OAuth2Stage))
- self.request.session[SESSION_KEY_PLAN] = plan
- return redirect_with_qs(
- "passbook_flows:flow-executor-shell",
- self.request.GET,
- flow_slug=provider.authorization_flow.slug,
- )
-
-
-class OAuth2Stage(AuthorizationView, StageView):
- """OAuth2 Stage, dynamically injected into the plan"""
-
- def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
- """Last stage in flow, finalizes OAuth Response and redirects to Client"""
- application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
- provider: OAuth2Provider = application.provider
-
- Event.new(
- EventAction.AUTHORIZE_APPLICATION, authorized_application=application,
- ).from_http(self.request)
-
- credentials = {
- "client_id": self.executor.plan.context[PLAN_CONTEXT_CLIENT_ID],
- "redirect_uri": self.executor.plan.context[PLAN_CONTEXT_REDIRECT_URI],
- "response_type": self.executor.plan.context.get(
- PLAN_CONTEXT_RESPONSE_TYPE, None
- ),
- "state": self.executor.plan.context.get(PLAN_CONTEXT_STATE, None),
- "nonce": self.executor.plan.context.get(PLAN_CONTEXT_NONCE, None),
- }
- if self.executor.plan.context.get(PLAN_CONTEXT_CODE_CHALLENGE, False):
- credentials[PLAN_CONTEXT_CODE_CHALLENGE] = self.executor.plan.context.get(
- PLAN_CONTEXT_CODE_CHALLENGE
- )
- if self.executor.plan.context.get(PLAN_CONTEXT_CODE_CHALLENGE_METHOD, False):
- credentials[
- PLAN_CONTEXT_CODE_CHALLENGE_METHOD
- ] = self.executor.plan.context.get(PLAN_CONTEXT_CODE_CHALLENGE_METHOD)
- scopes = self.executor.plan.context.get(PLAN_CONTEXT_SCOPE)
-
- try:
- uri, _headers, _body, _status = self.create_authorization_response(
- request=self.request,
- scopes=scopes,
- credentials=credentials,
- allow=True,
- )
- LOGGER.debug("Success url for the request: {0}".format(uri))
- except OAuthToolkitError as error:
- return self.error_response(error, provider)
-
- self.executor.stage_ok()
- return HttpResponseRedirect(self.redirect(uri, provider).url)
diff --git a/passbook/api/v1/__init__.py b/passbook/providers/oauth2/__init__.py
similarity index 100%
rename from passbook/api/v1/__init__.py
rename to passbook/providers/oauth2/__init__.py
diff --git a/passbook/providers/oauth2/api.py b/passbook/providers/oauth2/api.py
new file mode 100644
index 000000000..a4dad4461
--- /dev/null
+++ b/passbook/providers/oauth2/api.py
@@ -0,0 +1,50 @@
+"""OAuth2Provider API Views"""
+from rest_framework.serializers import ModelSerializer
+from rest_framework.viewsets import ModelViewSet
+
+from passbook.providers.oauth2.models import OAuth2Provider, ScopeMapping
+
+
+class OAuth2ProviderSerializer(ModelSerializer):
+ """OAuth2Provider Serializer"""
+
+ class Meta:
+
+ model = OAuth2Provider
+ fields = [
+ "pk",
+ "name",
+ "authorization_flow",
+ "client_type",
+ "client_id",
+ "client_secret",
+ "response_type",
+ "jwt_alg",
+ "rsa_key",
+ "redirect_uris",
+ "post_logout_redirect_uris",
+ "property_mappings",
+ ]
+
+
+class OAuth2ProviderViewSet(ModelViewSet):
+ """OAuth2Provider Viewset"""
+
+ queryset = OAuth2Provider.objects.all()
+ serializer_class = OAuth2ProviderSerializer
+
+
+class ScopeMappingSerializer(ModelSerializer):
+ """ScopeMapping Serializer"""
+
+ class Meta:
+
+ model = ScopeMapping
+ fields = ["pk", "name", "scope_name", "description", "expression"]
+
+
+class ScopeMappingViewSet(ModelViewSet):
+ """ScopeMapping Viewset"""
+
+ queryset = ScopeMapping.objects.all()
+ serializer_class = ScopeMappingSerializer
diff --git a/passbook/providers/oauth2/apps.py b/passbook/providers/oauth2/apps.py
new file mode 100644
index 000000000..e0cde620c
--- /dev/null
+++ b/passbook/providers/oauth2/apps.py
@@ -0,0 +1,14 @@
+"""passbook auth oauth provider app config"""
+from django.apps import AppConfig
+
+
+class PassbookProviderOAuth2Config(AppConfig):
+ """passbook auth oauth provider app config"""
+
+ name = "passbook.providers.oauth2"
+ label = "passbook_providers_oauth2"
+ verbose_name = "passbook Providers.OAuth2"
+ mountpoints = {
+ "passbook.providers.oauth2.urls": "application/o/",
+ "passbook.providers.oauth2.urls_github": "",
+ }
diff --git a/passbook/providers/oauth2/constants.py b/passbook/providers/oauth2/constants.py
new file mode 100644
index 000000000..1bd3379a3
--- /dev/null
+++ b/passbook/providers/oauth2/constants.py
@@ -0,0 +1,19 @@
+"""OAuth/OpenID Constants"""
+
+GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code"
+GRANT_TYPE_REFRESH_TOKEN = "refresh_token"
+PROMPT_NONE = "none"
+PROMPT_CONSNET = "consent"
+SCOPE_OPENID = "openid"
+SCOPE_OPENID_PROFILE = "profile"
+SCOPE_OPENID_EMAIL = "email"
+SCOPE_OPENID_INTROSPECTION = "token_introspection"
+
+# Read/write full user (including email)
+SCOPE_GITHUB_USER = "user"
+# Read user (without email)
+SCOPE_GITHUB_USER_READ = "read:user"
+# Read users email addresses
+SCOPE_GITHUB_USER_EMAIL = "user:email"
+# Read info about teams
+SCOPE_GITHUB_ORG_READ = "read:org"
diff --git a/passbook/providers/oauth2/errors.py b/passbook/providers/oauth2/errors.py
new file mode 100644
index 000000000..4e7be553d
--- /dev/null
+++ b/passbook/providers/oauth2/errors.py
@@ -0,0 +1,178 @@
+"""OAuth errors"""
+from urllib.parse import quote
+
+
+class OAuth2Error(Exception):
+ """Base class for all OAuth2 Errors"""
+
+ error: str
+ description: str
+
+ def create_dict(self):
+ """Return error as dict for JSON Rendering"""
+ return {
+ "error": self.error,
+ "error_description": self.description,
+ }
+
+ def __repr__(self) -> str:
+ return self.error
+
+
+class RedirectUriError(OAuth2Error):
+ """The request fails due to a missing, invalid, or mismatching
+ redirection URI (redirect_uri)."""
+
+ error = "Redirect URI Error"
+ description = (
+ "The request fails due to a missing, invalid, or mismatching"
+ " redirection URI (redirect_uri)."
+ )
+
+
+class ClientIdError(OAuth2Error):
+ """The client identifier (client_id) is missing or invalid."""
+
+ error = "Client ID Error"
+ description = "The client identifier (client_id) is missing or invalid."
+
+
+class UserAuthError(OAuth2Error):
+ """
+ Specific to the Resource Owner Password Credentials flow when
+ the Resource Owners credentials are not valid.
+ """
+
+ error = "access_denied"
+ description = "The resource owner or authorization server denied the request."
+
+
+class TokenIntrospectionError(OAuth2Error):
+ """
+ Specific to the introspection endpoint. This error will be converted
+ to an "active: false" response, as per the spec.
+ See https://tools.ietf.org/html/rfc7662
+ """
+
+
+class AuthorizeError(OAuth2Error):
+ """General Authorization Errors"""
+
+ _errors = {
+ # OAuth2 errors.
+ # https://tools.ietf.org/html/rfc6749#section-4.1.2.1
+ "invalid_request": "The request is otherwise malformed",
+ "unauthorized_client": "The client is not authorized to request an "
+ "authorization code using this method",
+ "access_denied": "The resource owner or authorization server denied "
+ "the request",
+ "unsupported_response_type": "The authorization server does not "
+ "support obtaining an authorization code "
+ "using this method",
+ "invalid_scope": "The requested scope is invalid, unknown, or " "malformed",
+ "server_error": "The authorization server encountered an error",
+ "temporarily_unavailable": "The authorization server is currently "
+ "unable to handle the request due to a "
+ "temporary overloading or maintenance of "
+ "the server",
+ # OpenID errors.
+ # http://openid.net/specs/openid-connect-core-1_0.html#AuthError
+ "interaction_required": "The Authorization Server requires End-User "
+ "interaction of some form to proceed",
+ "login_required": "The Authorization Server requires End-User "
+ "authentication",
+ "account_selection_required": "The End-User is required to select a "
+ "session at the Authorization Server",
+ "consent_required": "The Authorization Server requires End-User" "consent",
+ "invalid_request_uri": "The request_uri in the Authorization Request "
+ "returns an error or contains invalid data",
+ "invalid_request_object": "The request parameter contains an invalid "
+ "Request Object",
+ "request_not_supported": "The provider does not support use of the "
+ "request parameter",
+ "request_uri_not_supported": "The provider does not support use of the "
+ "request_uri parameter",
+ "registration_not_supported": "The provider does not support use of "
+ "the registration parameter",
+ }
+
+ def __init__(self, redirect_uri, error, grant_type):
+ super().__init__()
+ self.error = error
+ self.description = self._errors[error]
+ self.redirect_uri = redirect_uri
+ self.grant_type = grant_type
+
+ def create_uri(self, redirect_uri: str, state: str) -> str:
+ """Get a redirect URI with the error message"""
+ description = quote(str(self.description))
+
+ # See:
+ # http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError
+ hash_or_question = "#" if self.grant_type == "implicit" else "?"
+
+ uri = "{0}{1}error={2}&error_description={3}".format(
+ redirect_uri, hash_or_question, self.error, description
+ )
+
+ # Add state if present.
+ uri = uri + ("&state={0}".format(state) if state else "")
+
+ return uri
+
+
+class TokenError(OAuth2Error):
+ """
+ OAuth2 token endpoint errors.
+ https://tools.ietf.org/html/rfc6749#section-5.2
+ """
+
+ _errors = {
+ "invalid_request": "The request is otherwise malformed",
+ "invalid_client": "Client authentication failed (e.g., unknown client, "
+ "no client authentication included, or unsupported "
+ "authentication method)",
+ "invalid_grant": "The provided authorization grant or refresh token is "
+ "invalid, expired, revoked, does not match the "
+ "redirection URI used in the authorization request, "
+ "or was issued to another client",
+ "unauthorized_client": "The authenticated client is not authorized to "
+ "use this authorization grant type",
+ "unsupported_grant_type": "The authorization grant type is not "
+ "supported by the authorization server",
+ "invalid_scope": "The requested scope is invalid, unknown, malformed, "
+ "or exceeds the scope granted by the resource owner",
+ }
+
+ def __init__(self, error):
+ super().__init__()
+ self.error = error
+ self.description = self._errors[error]
+
+
+class BearerTokenError(OAuth2Error):
+ """
+ OAuth2 errors.
+ https://tools.ietf.org/html/rfc6750#section-3.1
+ """
+
+ _errors = {
+ "invalid_request": ("The request is otherwise malformed", 400),
+ "invalid_token": (
+ "The access token provided is expired, revoked, malformed, "
+ "or invalid for other reasons",
+ 401,
+ ),
+ "insufficient_scope": (
+ "The request requires higher privileges than provided by "
+ "the access token",
+ 403,
+ ),
+ }
+
+ def __init__(self, code):
+ super().__init__()
+ self.code = code
+ error_tuple = self._errors.get(code, ("", ""))
+ self.description = error_tuple[0]
+ self.status = error_tuple[1]
diff --git a/passbook/providers/oauth2/forms.py b/passbook/providers/oauth2/forms.py
new file mode 100644
index 000000000..296cc32fa
--- /dev/null
+++ b/passbook/providers/oauth2/forms.py
@@ -0,0 +1,80 @@
+"""passbook OAuth2 Provider Forms"""
+
+from django import forms
+from django.utils.translation import gettext as _
+
+from passbook.admin.fields import CodeMirrorWidget
+from passbook.core.expression import PropertyMappingEvaluator
+from passbook.crypto.models import CertificateKeyPair
+from passbook.flows.models import Flow, FlowDesignation
+from passbook.providers.oauth2.generators import (
+ generate_client_id,
+ generate_client_secret,
+)
+from passbook.providers.oauth2.models import OAuth2Provider, ScopeMapping
+
+
+class OAuth2ProviderForm(forms.ModelForm):
+ """OAuth2 Provider form"""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields["authorization_flow"].queryset = Flow.objects.filter(
+ designation=FlowDesignation.AUTHORIZATION
+ )
+ self.fields["client_id"].initial = generate_client_id()
+ self.fields["client_secret"].initial = generate_client_secret()
+ self.fields["rsa_key"].queryset = CertificateKeyPair.objects.exclude(
+ key_data__exact=""
+ )
+ self.fields["property_mappings"].queryset = ScopeMapping.objects.all()
+
+ class Meta:
+ model = OAuth2Provider
+ fields = [
+ "name",
+ "authorization_flow",
+ "client_type",
+ "client_id",
+ "client_secret",
+ "response_type",
+ "jwt_alg",
+ "rsa_key",
+ "redirect_uris",
+ "post_logout_redirect_uris",
+ "property_mappings",
+ ]
+ widgets = {
+ "name": forms.TextInput(),
+ }
+ labels = {"property_mappings": _("Scopes")}
+ help_texts = {
+ "property_mappings": _(
+ (
+ "Select which scopes can be used by the client. "
+ "The client stil has to specify the scope to access the data."
+ )
+ )
+ }
+
+
+class ScopeMappingForm(forms.ModelForm):
+ """Form to edit ScopeMappings"""
+
+ def clean_expression(self):
+ """Test Syntax"""
+ expression = self.cleaned_data.get("expression")
+ evaluator = PropertyMappingEvaluator()
+ evaluator.validate(expression)
+ return expression
+
+ class Meta:
+
+ model = ScopeMapping
+ fields = ["name", "scope_name", "description", "expression"]
+ widgets = {
+ "name": forms.TextInput(),
+ "scope_name": forms.TextInput(),
+ "description": forms.TextInput(),
+ "expression": CodeMirrorWidget(mode="python"),
+ }
diff --git a/passbook/providers/oauth2/generators.py b/passbook/providers/oauth2/generators.py
new file mode 100644
index 000000000..57df54ea8
--- /dev/null
+++ b/passbook/providers/oauth2/generators.py
@@ -0,0 +1,17 @@
+"""OAuth2 Client ID/Secret Generators"""
+import string
+from random import SystemRandom
+
+
+def generate_client_id():
+ """Generate a random client ID"""
+ rand = SystemRandom()
+ return "".join(rand.choice(string.ascii_letters + string.digits) for x in range(40))
+
+
+def generate_client_secret():
+ """Generate a suitable client secret"""
+ rand = SystemRandom()
+ return "".join(
+ rand.choice(string.ascii_letters + string.digits) for x in range(128)
+ )
diff --git a/passbook/providers/oauth2/migrations/0001_initial.py b/passbook/providers/oauth2/migrations/0001_initial.py
new file mode 100644
index 000000000..0fca333d9
--- /dev/null
+++ b/passbook/providers/oauth2/migrations/0001_initial.py
@@ -0,0 +1,357 @@
+# Generated by Django 3.1 on 2020-08-18 15:59
+
+import django.db.models.deletion
+from django.apps.registry import Apps
+from django.conf import settings
+from django.db import migrations, models
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+
+import passbook.core.models
+import passbook.lib.utils.time
+import passbook.providers.oauth2.generators
+
+SCOPE_OPENID_EXPRESSION = """# This is only required for OpenID Applications, but does not grant any information by itself.
+return {}
+"""
+SCOPE_EMAIL_EXPRESSION = """return {
+ "email": user.email,
+ "email_verified": True
+}
+"""
+SCOPE_PROFILE_EXPRESSION = """return {
+ "name": user.name,
+ "given_name": user.name,
+ "family_name": "",
+ "preferred_username": user.username,
+ "nickname": user.username,
+}
+"""
+
+
+def create_default_scopes(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
+ ScopeMapping = apps.get_model("passbook_providers_oauth2", "ScopeMapping")
+ ScopeMapping.objects.update_or_create(
+ scope_name="openid",
+ defaults={
+ "name": "Autogenerated OAuth2 Mapping: OpenID 'openid'",
+ "scope_name": "openid",
+ "description": "",
+ "expression": SCOPE_OPENID_EXPRESSION,
+ },
+ )
+ ScopeMapping.objects.update_or_create(
+ scope_name="email",
+ defaults={
+ "name": "Autogenerated OAuth2 Mapping: OpenID 'email'",
+ "scope_name": "email",
+ "description": "Email address",
+ "expression": SCOPE_EMAIL_EXPRESSION,
+ },
+ )
+ ScopeMapping.objects.update_or_create(
+ scope_name="profile",
+ defaults={
+ "name": "Autogenerated OAuth2 Mapping: OpenID 'profile'",
+ "scope_name": "profile",
+ "description": "General Profile Information",
+ "expression": SCOPE_PROFILE_EXPRESSION,
+ },
+ )
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("passbook_core", "0007_auto_20200815_1841"),
+ ("passbook_crypto", "0002_create_self_signed_kp"),
+ ]
+
+ operations = [
+ migrations.RunSQL(
+ "DROP TABLE IF EXISTS passbook_providers_oauth_oauth2provider CASCADE;"
+ ),
+ migrations.RunSQL(
+ "DROP TABLE IF EXISTS passbook_providers_oidc_openidprovider CASCADE;"
+ ),
+ migrations.CreateModel(
+ name="OAuth2Provider",
+ fields=[
+ (
+ "provider_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="passbook_core.provider",
+ ),
+ ),
+ ("name", models.TextField()),
+ (
+ "client_type",
+ models.CharField(
+ choices=[
+ ("confidential", "Confidential"),
+ ("public", "Public"),
+ ],
+ default="confidential",
+ help_text="Confidential clients are capable of maintaining the confidentiality\n of their credentials. Public clients are incapable.",
+ max_length=30,
+ verbose_name="Client Type",
+ ),
+ ),
+ (
+ "client_id",
+ models.CharField(
+ default=passbook.providers.oauth2.generators.generate_client_id,
+ max_length=255,
+ unique=True,
+ verbose_name="Client ID",
+ ),
+ ),
+ (
+ "client_secret",
+ models.CharField(
+ blank=True,
+ default=passbook.providers.oauth2.generators.generate_client_secret,
+ max_length=255,
+ verbose_name="Client Secret",
+ ),
+ ),
+ (
+ "response_type",
+ models.TextField(
+ choices=[
+ ("code", "code (Authorization Code Flow)"),
+ ("id_token", "id_token (Implicit Flow)"),
+ ("id_token token", "id_token token (Implicit Flow)"),
+ ("code token", "code token (Hybrid Flow)"),
+ ("code id_token", "code id_token (Hybrid Flow)"),
+ (
+ "code id_token token",
+ "code id_token token (Hybrid Flow)",
+ ),
+ ],
+ default="code",
+ help_text="Response Type required by the client.",
+ ),
+ ),
+ (
+ "jwt_alg",
+ models.CharField(
+ choices=[
+ ("HS256", "HS256 (Symmetric Encryption)"),
+ ("RS256", "RS256 (Asymmetric Encryption)"),
+ ],
+ default="RS256",
+ help_text="Algorithm used to sign the JWT Token",
+ max_length=10,
+ verbose_name="JWT Algorithm",
+ ),
+ ),
+ (
+ "redirect_uris",
+ models.TextField(
+ default="",
+ help_text="Enter each URI on a new line.",
+ verbose_name="Redirect URIs",
+ ),
+ ),
+ (
+ "post_logout_redirect_uris",
+ models.TextField(
+ blank=True,
+ default="",
+ help_text="Enter each URI on a new line.",
+ verbose_name="Post Logout Redirect URIs",
+ ),
+ ),
+ (
+ "include_claims_in_id_token",
+ models.BooleanField(
+ default=True,
+ help_text="Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
+ verbose_name="Include claims in id_token",
+ ),
+ ),
+ (
+ "token_validity",
+ models.TextField(
+ default="minutes=10",
+ help_text="Tokens not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3).",
+ validators=[passbook.lib.utils.time.timedelta_string_validator],
+ ),
+ ),
+ (
+ "rsa_key",
+ models.ForeignKey(
+ help_text="Key used to sign the tokens. Only required when JWT Algorithm is set to RS256.",
+ on_delete=django.db.models.deletion.CASCADE,
+ to="passbook_crypto.certificatekeypair",
+ verbose_name="RSA Key",
+ blank=True,
+ null=True,
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "OAuth2/OpenID Provider",
+ "verbose_name_plural": "OAuth2/OpenID Providers",
+ },
+ bases=("passbook_core.provider",),
+ ),
+ migrations.CreateModel(
+ name="ScopeMapping",
+ fields=[
+ (
+ "propertymapping_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="passbook_core.propertymapping",
+ ),
+ ),
+ ("scope_name", models.TextField(help_text="Scope used by the client")),
+ (
+ "description",
+ models.TextField(
+ blank=True,
+ help_text="Description shown to the user when consenting. If left empty, the user won't be informed.",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "Scope Mapping",
+ "verbose_name_plural": "Scope Mappings",
+ },
+ bases=("passbook_core.propertymapping",),
+ ),
+ migrations.RunPython(create_default_scopes),
+ migrations.CreateModel(
+ name="RefreshToken",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "expires",
+ models.DateTimeField(
+ default=passbook.core.models.default_token_duration
+ ),
+ ),
+ ("expiring", models.BooleanField(default=True)),
+ ("_scope", models.TextField(default="", verbose_name="Scopes")),
+ (
+ "access_token",
+ models.CharField(
+ max_length=255, unique=True, verbose_name="Access Token"
+ ),
+ ),
+ (
+ "refresh_token",
+ models.CharField(
+ max_length=255, unique=True, verbose_name="Refresh Token"
+ ),
+ ),
+ ("_id_token", models.TextField(verbose_name="ID Token")),
+ (
+ "provider",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="passbook_providers_oauth2.oauth2provider",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="User",
+ ),
+ ),
+ ],
+ options={"verbose_name": "Token", "verbose_name_plural": "Tokens",},
+ ),
+ migrations.CreateModel(
+ name="AuthorizationCode",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "expires",
+ models.DateTimeField(
+ default=passbook.core.models.default_token_duration
+ ),
+ ),
+ ("expiring", models.BooleanField(default=True)),
+ ("_scope", models.TextField(default="", verbose_name="Scopes")),
+ (
+ "code",
+ models.CharField(max_length=255, unique=True, verbose_name="Code"),
+ ),
+ (
+ "nonce",
+ models.CharField(
+ blank=True, default="", max_length=255, verbose_name="Nonce"
+ ),
+ ),
+ (
+ "is_open_id",
+ models.BooleanField(
+ default=False, verbose_name="Is Authentication?"
+ ),
+ ),
+ (
+ "code_challenge",
+ models.CharField(
+ max_length=255, null=True, verbose_name="Code Challenge"
+ ),
+ ),
+ (
+ "code_challenge_method",
+ models.CharField(
+ max_length=255, null=True, verbose_name="Code Challenge Method"
+ ),
+ ),
+ (
+ "provider",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="passbook_providers_oauth2.oauth2provider",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="User",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "Authorization Code",
+ "verbose_name_plural": "Authorization Codes",
+ },
+ ),
+ ]
diff --git a/passbook/providers/app_gw/__init__.py b/passbook/providers/oauth2/migrations/__init__.py
similarity index 100%
rename from passbook/providers/app_gw/__init__.py
rename to passbook/providers/oauth2/migrations/__init__.py
diff --git a/passbook/providers/oauth2/models.py b/passbook/providers/oauth2/models.py
new file mode 100644
index 000000000..2e89d82ee
--- /dev/null
+++ b/passbook/providers/oauth2/models.py
@@ -0,0 +1,445 @@
+"""OAuth Provider Models"""
+import base64
+import binascii
+import json
+import time
+from dataclasses import asdict, dataclass, field
+from hashlib import sha256
+from typing import Any, Dict, List, Optional, Type
+from uuid import uuid4
+
+from django.conf import settings
+from django.db import models
+from django.forms import ModelForm
+from django.http import HttpRequest
+from django.shortcuts import reverse
+from django.utils import dateformat, timezone
+from django.utils.translation import ugettext_lazy as _
+from jwkest.jwk import Key, RSAKey, SYMKey, import_rsa_key
+from jwkest.jws import JWS
+
+from passbook.core.models import ExpiringModel, PropertyMapping, Provider, User
+from passbook.crypto.models import CertificateKeyPair
+from passbook.lib.utils.template import render_to_string
+from passbook.lib.utils.time import timedelta_from_string, timedelta_string_validator
+from passbook.providers.oauth2.apps import PassbookProviderOAuth2Config
+from passbook.providers.oauth2.generators import (
+ generate_client_id,
+ generate_client_secret,
+)
+
+
+class ClientTypes(models.TextChoices):
+ """Confidential clients are capable of maintaining the confidentiality
+ of their credentials. Public clients are incapable."""
+
+ CONFIDENTIAL = "confidential", _("Confidential")
+ PUBLIC = "public", _("Public")
+
+
+class GrantTypes(models.TextChoices):
+ """OAuth2 Grant types we support"""
+
+ AUTHORIZATION_CODE = "authorization_code"
+ IMPLICIT = "implicit"
+ HYBRID = "hybrid"
+
+
+class ResponseTypes(models.TextChoices):
+ """Response Type required by the client."""
+
+ CODE = "code", _("code (Authorization Code Flow)")
+ ID_TOKEN = "id_token", _("id_token (Implicit Flow)")
+ ID_TOKEN_TOKEN = "id_token token", _("id_token token (Implicit Flow)")
+ CODE_TOKEN = "code token", _("code token (Hybrid Flow)")
+ CODE_ID_TOKEN = "code id_token", _("code id_token (Hybrid Flow)")
+ CODE_ID_TOKEN_TOKEN = "code id_token token", _("code id_token token (Hybrid Flow)")
+
+
+class JWTAlgorithms(models.TextChoices):
+ """Algorithm used to sign the JWT Token"""
+
+ HS256 = "HS256", _("HS256 (Symmetric Encryption)")
+ RS256 = "RS256", _("RS256 (Asymmetric Encryption)")
+
+
+class ScopeMapping(PropertyMapping):
+ """Map an OAuth Scope to users properties"""
+
+ scope_name = models.TextField(help_text=_("Scope used by the client"))
+ description = models.TextField(
+ blank=True,
+ help_text=_(
+ (
+ "Description shown to the user when consenting. "
+ "If left empty, the user won't be informed."
+ )
+ ),
+ )
+
+ def form(self) -> Type[ModelForm]:
+ from passbook.providers.oauth2.forms import ScopeMappingForm
+
+ return ScopeMappingForm
+
+ def __str__(self):
+ return f"Scope Mapping '{self.scope_name}'"
+
+ class Meta:
+
+ verbose_name = _("Scope Mapping")
+ verbose_name_plural = _("Scope Mappings")
+
+
+class OAuth2Provider(Provider):
+ """OAuth2 Provider for generic OAuth and OpenID Connect Applications."""
+
+ name = models.TextField()
+
+ client_type = models.CharField(
+ max_length=30,
+ choices=ClientTypes.choices,
+ default=ClientTypes.CONFIDENTIAL,
+ verbose_name=_("Client Type"),
+ help_text=_(ClientTypes.__doc__),
+ )
+ client_id = models.CharField(
+ max_length=255,
+ unique=True,
+ verbose_name=_("Client ID"),
+ default=generate_client_id,
+ )
+ client_secret = models.CharField(
+ max_length=255,
+ blank=True,
+ verbose_name=_("Client Secret"),
+ default=generate_client_secret,
+ )
+ response_type = models.TextField(
+ choices=ResponseTypes.choices,
+ default=ResponseTypes.CODE,
+ help_text=_(ResponseTypes.__doc__),
+ )
+ jwt_alg = models.CharField(
+ max_length=10,
+ choices=JWTAlgorithms.choices,
+ default=JWTAlgorithms.RS256,
+ verbose_name=_("JWT Algorithm"),
+ help_text=_(JWTAlgorithms.__doc__),
+ )
+ redirect_uris = models.TextField(
+ default="",
+ verbose_name=_("Redirect URIs"),
+ help_text=_("Enter each URI on a new line."),
+ )
+ post_logout_redirect_uris = models.TextField(
+ blank=True,
+ default="",
+ verbose_name=_("Post Logout Redirect URIs"),
+ help_text=_("Enter each URI on a new line."),
+ )
+
+ include_claims_in_id_token = models.BooleanField(
+ default=True,
+ verbose_name=_("Include claims in id_token"),
+ help_text=_(
+ (
+ "Include User claims from scopes in the id_token, for applications "
+ "that don't access the userinfo endpoint."
+ )
+ ),
+ )
+
+ token_validity = models.TextField(
+ default="minutes=10",
+ validators=[timedelta_string_validator],
+ help_text=_(
+ (
+ "Tokens not valid on or after current time + this value "
+ "(Format: hours=1;minutes=2;seconds=3)."
+ )
+ ),
+ )
+
+ rsa_key = models.ForeignKey(
+ CertificateKeyPair,
+ verbose_name=_("RSA Key"),
+ on_delete=models.CASCADE,
+ blank=True,
+ null=True,
+ help_text=_(
+ "Key used to sign the tokens. Only required when JWT Algorithm is set to RS256."
+ ),
+ )
+
+ @property
+ def scope_names(self) -> List[str]:
+ """Return list of assigned scopes seperated with a space"""
+ return [pm.scope_name for pm in self.property_mappings.all()]
+
+ def create_refresh_token(
+ self, user: User, scope: List[str], id_token: Optional["IDToken"] = None
+ ) -> "RefreshToken":
+ """Create and populate a RefreshToken object."""
+ token = RefreshToken(
+ user=user,
+ provider=self,
+ access_token=uuid4().hex,
+ refresh_token=uuid4().hex,
+ expires=timezone.now() + timedelta_from_string(self.token_validity),
+ scope=scope,
+ )
+ if id_token:
+ token.id_token = id_token
+ return token
+
+ def get_jwt_keys(self) -> List[Key]:
+ """
+ Takes a provider and returns the set of keys associated with it.
+ Returns a list of keys.
+ """
+ if self.jwt_alg == JWTAlgorithms.RS256:
+ # if the user selected RS256 but didn't select a
+ # CertificateKeyPair, we fall back to HS256
+ if not self.rsa_key:
+ self.jwt_alg = JWTAlgorithms.HS256
+ self.save()
+ else:
+ # Because the JWT Library uses python cryptodome,
+ # we can't directly pass the RSAPublicKey
+ # object, but have to load it ourselves
+ key = import_rsa_key(self.rsa_key.key_data)
+ keys = [RSAKey(key=key, kid=self.rsa_key.kid)]
+ if not keys:
+ raise Exception("You must add at least one RSA Key.")
+ return keys
+
+ if self.jwt_alg == JWTAlgorithms.HS256:
+ return [SYMKey(key=self.client_secret, alg=self.jwt_alg)]
+
+ raise Exception("Unsupported key algorithm.")
+
+ def get_issuer(self, request: HttpRequest) -> Optional[str]:
+ """Get issuer, based on request"""
+ try:
+ mountpoint = PassbookProviderOAuth2Config.mountpoints[
+ "passbook.providers.oauth2.urls"
+ ]
+ # pylint: disable=no-member
+ return request.build_absolute_uri(f"/{mountpoint}{self.application.slug}/")
+ except Provider.application.RelatedObjectDoesNotExist:
+ return None
+
+ def form(self) -> Type[ModelForm]:
+ from passbook.providers.oauth2.forms import OAuth2ProviderForm
+
+ return OAuth2ProviderForm
+
+ def __str__(self):
+ return self.name
+
+ def html_setup_urls(self, request: HttpRequest) -> Optional[str]:
+ """return template and context modal with URLs for authorize, token, openid-config, etc"""
+ try:
+ # pylint: disable=no-member
+ return render_to_string(
+ "providers/oauth2/setup_url_modal.html",
+ {
+ "provider": self,
+ "authorize": request.build_absolute_uri(
+ reverse("passbook_providers_oauth2:authorize",)
+ ),
+ "token": request.build_absolute_uri(
+ reverse("passbook_providers_oauth2:token",)
+ ),
+ "userinfo": request.build_absolute_uri(
+ reverse("passbook_providers_oauth2:userinfo",)
+ ),
+ "provider_info": request.build_absolute_uri(
+ reverse(
+ "passbook_providers_oauth2:provider-info",
+ kwargs={"application_slug": self.application.slug},
+ )
+ ),
+ },
+ )
+ except Provider.application.RelatedObjectDoesNotExist:
+ return None
+
+ class Meta:
+
+ verbose_name = _("OAuth2/OpenID Provider")
+ verbose_name_plural = _("OAuth2/OpenID Providers")
+
+
+class BaseGrantModel(models.Model):
+ """Base Model for all grants"""
+
+ provider = models.ForeignKey(OAuth2Provider, on_delete=models.CASCADE)
+ user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
+ _scope = models.TextField(default="", verbose_name=_("Scopes"))
+
+ @property
+ def scope(self) -> List[str]:
+ """Return scopes as list of strings"""
+ return self._scope.split()
+
+ @scope.setter
+ def scope(self, value):
+ self._scope = " ".join(value)
+
+ class Meta:
+ abstract = True
+
+
+# pylint: disable=too-many-instance-attributes
+class AuthorizationCode(ExpiringModel, BaseGrantModel):
+ """OAuth2 Authorization Code"""
+
+ code = models.CharField(max_length=255, unique=True, verbose_name=_("Code"))
+ nonce = models.CharField(
+ max_length=255, blank=True, default="", verbose_name=_("Nonce")
+ )
+ is_open_id = models.BooleanField(
+ default=False, verbose_name=_("Is Authentication?")
+ )
+ code_challenge = models.CharField(
+ max_length=255, null=True, verbose_name=_("Code Challenge")
+ )
+ code_challenge_method = models.CharField(
+ max_length=255, null=True, verbose_name=_("Code Challenge Method")
+ )
+
+ class Meta:
+ verbose_name = _("Authorization Code")
+ verbose_name_plural = _("Authorization Codes")
+
+ def __str__(self):
+ return "{0} - {1}".format(self.provider, self.code)
+
+
+@dataclass
+# plyint: disable=too-many-instance-attributes
+class IDToken:
+ """The primary extension that OpenID Connect makes to OAuth 2.0 to enable End-Users to be
+ Authenticated is the ID Token data structure. The ID Token is a security token that contains
+ Claims about the Authentication of an End-User by an Authorization Server when using a Client,
+ and potentially other requested Claims. The ID Token is represented as a
+ JSON Web Token (JWT) [JWT].
+
+ https://openid.net/specs/openid-connect-core-1_0.html#IDToken"""
+
+ # All these fields need to optional so we can save an empty IDToken for non-OpenID flows.
+ iss: Optional[str] = None
+ sub: Optional[str] = None
+ aud: Optional[str] = None
+ exp: Optional[int] = None
+ iat: Optional[int] = None
+ auth_time: Optional[int] = None
+
+ nonce: Optional[str] = None
+ at_hash: Optional[str] = None
+
+ claims: Dict[str, Any] = field(default_factory=dict)
+
+ @staticmethod
+ def from_dict(data: Dict[str, Any]) -> "IDToken":
+ """Reconstruct ID Token from json dictionary"""
+ token = IDToken()
+ for key, value in data.items():
+ setattr(token, key, value)
+ return token
+
+ def to_dict(self) -> Dict[str, Any]:
+ """Convert dataclass to dict, and update with keys from `claims`"""
+ dic = asdict(self)
+ dic.pop("claims")
+ dic.update(self.claims)
+ return dic
+
+ def encode(self, provider: OAuth2Provider) -> str:
+ """Represent the ID Token as a JSON Web Token (JWT)."""
+ keys = provider.get_jwt_keys()
+ # If the provider does not have an RSA Key assigned, it was switched to Symmetric
+ provider.refresh_from_db()
+ jws = JWS(self.to_dict(), alg=provider.jwt_alg)
+ return jws.sign_compact(keys)
+
+
+class RefreshToken(ExpiringModel, BaseGrantModel):
+ """OAuth2 Refresh Token"""
+
+ access_token = models.CharField(
+ max_length=255, unique=True, verbose_name=_("Access Token")
+ )
+ refresh_token = models.CharField(
+ max_length=255, unique=True, verbose_name=_("Refresh Token")
+ )
+ _id_token = models.TextField(verbose_name=_("ID Token"))
+
+ class Meta:
+ verbose_name = _("Token")
+ verbose_name_plural = _("Tokens")
+
+ @property
+ def id_token(self) -> IDToken:
+ """Load ID Token from json"""
+ if self._id_token:
+ raw_token = json.loads(self._id_token)
+ return IDToken.from_dict(raw_token)
+ return IDToken()
+
+ @id_token.setter
+ def id_token(self, value: IDToken):
+ self._id_token = json.dumps(asdict(value))
+
+ def __str__(self):
+ return f"{self.provider} - {self.access_token}"
+
+ @property
+ def at_hash(self):
+ """Get hashed access_token"""
+ hashed_access_token = (
+ sha256(self.access_token.encode("ascii")).hexdigest().encode("ascii")
+ )
+ return (
+ base64.urlsafe_b64encode(
+ binascii.unhexlify(hashed_access_token[: len(hashed_access_token) // 2])
+ )
+ .rstrip(b"=")
+ .decode("ascii")
+ )
+
+ def create_id_token(self, user: User, request: HttpRequest) -> IDToken:
+ """Creates the id_token.
+ See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken"""
+ sub = sha256(f"{user.id}-{settings.SECRET_KEY}".encode("ascii")).hexdigest()
+
+ # Convert datetimes into timestamps.
+ now = int(time.time())
+ iat_time = now
+ exp_time = int(
+ now + timedelta_from_string(self.provider.token_validity).seconds
+ )
+ user_auth_time = user.last_login or user.date_joined
+ auth_time = int(dateformat.format(user_auth_time, "U"))
+
+ token = IDToken(
+ iss=self.provider.get_issuer(request),
+ sub=sub,
+ aud=self.provider.client_id,
+ exp=exp_time,
+ iat=iat_time,
+ auth_time=auth_time,
+ )
+
+ # Include (or not) user standard claims in the id_token.
+ if self.provider.include_claims_in_id_token:
+ from passbook.providers.oauth2.views.userinfo import UserInfoView
+
+ user_info = UserInfoView()
+ user_info.request = request
+ claims = user_info.get_claims(self)
+ token.claims = claims
+
+ return token
diff --git a/passbook/providers/oauth/templates/providers/oauth/consent.html b/passbook/providers/oauth2/templates/providers/oauth2/consent.html
similarity index 100%
rename from passbook/providers/oauth/templates/providers/oauth/consent.html
rename to passbook/providers/oauth2/templates/providers/oauth2/consent.html
diff --git a/passbook/providers/oidc/templates/oidc_provider/setup_url_modal.html b/passbook/providers/oauth2/templates/providers/oauth2/setup_url_modal.html
similarity index 94%
rename from passbook/providers/oidc/templates/oidc_provider/setup_url_modal.html
rename to passbook/providers/oauth2/templates/providers/oauth2/setup_url_modal.html
index 1805247f8..c653dabbe 100644
--- a/passbook/providers/oidc/templates/oidc_provider/setup_url_modal.html
+++ b/passbook/providers/oauth2/templates/providers/oauth2/setup_url_modal.html
@@ -1,8 +1,8 @@
{% load i18n %}
-
+
-
+