diff --git a/Pipfile b/Pipfile index 17497132..0079962c 100644 --- a/Pipfile +++ b/Pipfile @@ -38,6 +38,8 @@ xlrd = "*" ldap3 = "*" gunicorn = "*" werkzeug = "*" +sentry-sdk = {extras = ["flask"],version = "==0.14.4"} +flask-mail = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index d9c2bfab..fb38d03c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "979f996148ee181e3e0af2a3777aa1d00d0fd5d943d49df65963e694b8a88871" + "sha256": "6c89585086260ebcb41918b8ef3b1d9e189e1b492208d3ff000a138bc2f2fcee" }, "pipfile-spec": 6, "requires": { @@ -32,10 +32,10 @@ }, "amqp": { "hashes": [ - "sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8", - "sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d" + "sha256:24dbaff8ce4f30566bb88976b398e8c4e77637171af3af6f1b9650f48890e60b", + "sha256:bb68f8d2bced8f93ccfd07d96c689b716b3227720add971be980accfc2952139" ], - "version": "==2.5.2" + "version": "==2.6.0" }, "aniso8601": { "hashes": [ @@ -96,19 +96,25 @@ ], "version": "==3.6.3.0" }, + "blinker": { + "hashes": [ + "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6" + ], + "version": "==1.4" + }, "celery": { "hashes": [ - "sha256:108a0bf9018a871620936c33a3ee9f6336a89f8ef0a0f567a9001f4aa361415f", - "sha256:5b4b37e276033fe47575107a2775469f0b721646a08c96ec2c61531e4fe45f2a" + "sha256:c3f4173f83ceb5a5c986c5fdaefb9456de3b0729a72a5776e46bd405fda7b647", + "sha256:d1762d6065522879f341c3d67c2b9fe4615eb79756d59acb1434601d4aca474b" ], - "version": "==4.4.2" + "version": "==4.4.5" }, "certifi": { "hashes": [ - "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", - "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1", + "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc" ], - "version": "==2020.4.5.1" + "version": "==2020.4.5.2" }, "cffi": { "hashes": [ @@ -270,13 +276,20 @@ "index": "pypi", "version": "==3.0.8" }, - "flask-marshmallow": { + "flask-mail": { "hashes": [ - "sha256:6e6aec171b8e092e0eafaf035ff5b8637bf3a58ab46f568c4c1bab02f2a3c196", - "sha256:a1685536e7ab5abdc712bbc1ac1a6b0b50951a368502f7985e7d1c27b3c21e59" + "sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41" ], "index": "pypi", - "version": "==0.12.0" + "version": "==0.9.1" + }, + "flask-marshmallow": { + "hashes": [ + "sha256:1da1e6454a56a3e15107b987121729f152325bdef23f3df2f9b52bbd074af38e", + "sha256:aefc1f1d96256c430a409f08241bab75ffe97e5d14ac5d1f000764e39bf4873a" + ], + "index": "pypi", + "version": "==0.13.0" }, "flask-migrate": { "hashes": [ @@ -338,18 +351,18 @@ }, "importlib-metadata": { "hashes": [ - "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", - "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" + "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545", + "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958" ], "markers": "python_version < '3.8'", - "version": "==1.6.0" + "version": "==1.6.1" }, "inflection": { "hashes": [ - "sha256:32a5c3341d9583ec319548b9015b7fbdf8c429cbcb575d326c33ae3a0e90d52c", - "sha256:9a15d3598f01220e93f2207c432cfede50daff53137ce660fb8be838ef1ca6cc" + "sha256:88b101b2668a1d81d6d72d4c2018e53bc6c7fc544c987849da1c7f77545c3bc9", + "sha256:f576e85132d34f5bf7df5183c2c6f94cfb32e528f53065345cf71329ba0b8924" ], - "version": "==0.4.0" + "version": "==0.5.0" }, "itsdangerous": { "hashes": [ @@ -381,10 +394,10 @@ }, "kombu": { "hashes": [ - "sha256:2d1cda774126a044d91a7ff5fa6d09edf99f46924ab332a810760fe6740e9b76", - "sha256:598e7e749d6ab54f646b74b2d2df67755dee13894f73ab02a2a9feb8870c7cb2" + "sha256:437b9cdea193cc2ed0b8044c85fd0f126bb3615ca2f4d4a35b39de7cacfa3c1a", + "sha256:dc282bb277197d723bccda1a9ba30a27a28c9672d0ab93e9e51bb05a37bd29c3" ], - "version": "==4.6.8" + "version": "==4.6.10" }, "ldap3": { "hashes": [ @@ -428,10 +441,10 @@ }, "mako": { "hashes": [ - "sha256:3139c5d64aa5d175dbafb95027057128b5fbd05a40c53999f3905ceb53366d9d", - "sha256:8e8b53c71c7e59f3de716b6832c4e401d903af574f6962edbbbf6ecc2a5fe6c9" + "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27", + "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9" ], - "version": "==1.1.2" + "version": "==1.1.3" }, "markupsafe": { "hashes": [ @@ -473,11 +486,11 @@ }, "marshmallow": { "hashes": [ - "sha256:c2673233aa21dde264b84349dc2fd1dce5f30ed724a0a00e75426734de5b84ab", - "sha256:f88fe96434b1f0f476d54224d59333eba8ca1a203a2695683c1855675c4049a7" + "sha256:35ee2fb188f0bd9fc1cf9ac35e45fd394bd1c153cee430745a465ea435514bd5", + "sha256:9aa20f9b71c992b4782dad07c51d92884fd0f7c5cb9d3c737bea17ec1bad765f" ], "index": "pypi", - "version": "==3.6.0" + "version": "==3.6.1" }, "marshmallow-enum": { "hashes": [ @@ -489,37 +502,37 @@ }, "marshmallow-sqlalchemy": { "hashes": [ - "sha256:3247e41e424146340b03a369f2b7c6f0364477ccedc4e2481e84d5f3a8d3c67f", - "sha256:dbbe51d28bb28e7ee2782e51310477f7a2c5a111a301f6dd8e264e11ab820427" + "sha256:03a555b610bb307689b821b64e2416593ec21a85925c8c436c2cd08ebc6bb85e", + "sha256:0ef59c8da8da2e18e808e3880158049e9d72f3031c84cc804b6c533a0eb668a9" ], "index": "pypi", - "version": "==0.23.0" + "version": "==0.23.1" }, "numpy": { "hashes": [ - "sha256:00d7b54c025601e28f468953d065b9b121ddca7fff30bed7be082d3656dd798d", - "sha256:02ec9582808c4e48be4e93cd629c855e644882faf704bc2bd6bbf58c08a2a897", - "sha256:0e6f72f7bb08f2f350ed4408bb7acdc0daba637e73bce9f5ea2b207039f3af88", - "sha256:1be2e96314a66f5f1ce7764274327fd4fb9da58584eaff00b5a5221edefee7d6", - "sha256:2466fbcf23711ebc5daa61d28ced319a6159b260a18839993d871096d66b93f7", - "sha256:2b573fcf6f9863ce746e4ad00ac18a948978bb3781cffa4305134d31801f3e26", - "sha256:3f0dae97e1126f529ebb66f3c63514a0f72a177b90d56e4bce8a0b5def34627a", - "sha256:50fb72bcbc2cf11e066579cb53c4ca8ac0227abb512b6cbc1faa02d1595a2a5d", - "sha256:57aea170fb23b1fd54fa537359d90d383d9bf5937ee54ae8045a723caa5e0961", - "sha256:709c2999b6bd36cdaf85cf888d8512da7433529f14a3689d6e37ab5242e7add5", - "sha256:7d59f21e43bbfd9a10953a7e26b35b6849d888fc5a331fa84a2d9c37bd9fe2a2", - "sha256:904b513ab8fbcbdb062bed1ce2f794ab20208a1b01ce9bd90776c6c7e7257032", - "sha256:96dd36f5cdde152fd6977d1bbc0f0561bccffecfde63cd397c8e6033eb66baba", - "sha256:9933b81fecbe935e6a7dc89cbd2b99fea1bf362f2790daf9422a7bb1dc3c3085", - "sha256:bbcc85aaf4cd84ba057decaead058f43191cc0e30d6bc5d44fe336dc3d3f4509", - "sha256:dccd380d8e025c867ddcb2f84b439722cf1f23f3a319381eac45fd077dee7170", - "sha256:e22cd0f72fc931d6abc69dc7764484ee20c6a60b0d0fee9ce0426029b1c1bdae", - "sha256:ed722aefb0ebffd10b32e67f48e8ac4c5c4cf5d3a785024fdf0e9eb17529cd9d", - "sha256:efb7ac5572c9a57159cf92c508aad9f856f1cb8e8302d7fdb99061dbe52d712c", - "sha256:efdba339fffb0e80fcc19524e4fdbda2e2b5772ea46720c44eaac28096d60720", - "sha256:f22273dd6a403ed870207b853a856ff6327d5cbce7a835dfa0645b3fc00273ec" + "sha256:0172304e7d8d40e9e49553901903dc5f5a49a703363ed756796f5808a06fc233", + "sha256:34e96e9dae65c4839bd80012023aadd6ee2ccb73ce7fdf3074c62f301e63120b", + "sha256:3676abe3d621fc467c4c1469ee11e395c82b2d6b5463a9454e37fe9da07cd0d7", + "sha256:3dd6823d3e04b5f223e3e265b4a1eae15f104f4366edd409e5a5e413a98f911f", + "sha256:4064f53d4cce69e9ac613256dc2162e56f20a4e2d2086b1956dd2fcf77b7fac5", + "sha256:4674f7d27a6c1c52a4d1aa5f0881f1eff840d2206989bae6acb1c7668c02ebfb", + "sha256:7d42ab8cedd175b5ebcb39b5208b25ba104842489ed59fbb29356f671ac93583", + "sha256:965df25449305092b23d5145b9bdaeb0149b6e41a77a7d728b1644b3c99277c1", + "sha256:9c9d6531bc1886454f44aa8f809268bc481295cf9740827254f53c30104f074a", + "sha256:a78e438db8ec26d5d9d0e584b27ef25c7afa5a182d1bf4d05e313d2d6d515271", + "sha256:a7acefddf994af1aeba05bbbafe4ba983a187079f125146dc5859e6d817df824", + "sha256:a87f59508c2b7ceb8631c20630118cc546f1f815e034193dc72390db038a5cb3", + "sha256:ac792b385d81151bae2a5a8adb2b88261ceb4976dbfaaad9ce3a200e036753dc", + "sha256:b03b2c0badeb606d1232e5f78852c102c0a7989d3a534b3129e7856a52f3d161", + "sha256:b39321f1a74d1f9183bf1638a745b4fd6fe80efbb1f6b32b932a588b4bc7695f", + "sha256:cae14a01a159b1ed91a324722d746523ec757357260c6804d11d6147a9e53e3f", + "sha256:cd49930af1d1e49a812d987c2620ee63965b619257bd76eaaa95870ca08837cf", + "sha256:e15b382603c58f24265c9c931c9a45eebf44fe2e6b4eaedbb0d025ab3255228b", + "sha256:e91d31b34fc7c2c8f756b4e902f901f856ae53a93399368d9a0dc7be17ed2ca0", + "sha256:ef627986941b5edd1ed74ba89ca43196ed197f1a206a3f18cc9faf2fb84fd675", + "sha256:f718a7949d1c4f622ff548c572e0c03440b49b9531ff00e4ed5738b459f011e8" ], - "version": "==1.18.4" + "version": "==1.18.5" }, "openapi-spec-validator": { "hashes": [ @@ -704,6 +717,17 @@ "index": "pypi", "version": "==2.23.0" }, + "sentry-sdk": { + "extras": [ + "flask" + ], + "hashes": [ + "sha256:0e5e947d0f7a969314aa23669a94a9712be5a688ff069ff7b9fc36c66adc160c", + "sha256:799a8bf76b012e3030a881be00e97bc0b922ce35dde699c6537122b751d80e2c" + ], + "index": "pypi", + "version": "==0.14.4" + }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", @@ -727,11 +751,11 @@ }, "sphinx": { "hashes": [ - "sha256:779a519adbd3a70fc7c468af08c5e74829868b0a5b34587b33340e010291856c", - "sha256:ea64df287958ee5aac46be7ac2b7277305b0381d213728c3a49d8bb9b8415807" + "sha256:1c445320a3310baa5ccb8d957267ef4a0fc930dc1234db5098b3d7af14fbb242", + "sha256:7d3d5087e39ab5a031b75588e9859f011de70e213cd0080ccbc28079fb0786d1" ], "index": "pypi", - "version": "==3.0.4" + "version": "==3.1.0" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -778,7 +802,7 @@ "spiffworkflow": { "editable": true, "git": "https://github.com/sartography/SpiffWorkflow.git", - "ref": "c8d87826d496af825a184bdc3f0a751e603cfe44" + "ref": "b8a064a0bb76c705a1be04ee9bb8ac7beee56eb0" }, "sqlalchemy": { "hashes": [ @@ -838,10 +862,10 @@ }, "waitress": { "hashes": [ - "sha256:045b3efc3d97c93362173ab1dfc159b52cfa22b46c3334ffc805dbdbf0e4309e", - "sha256:77ff3f3226931a1d7d8624c5371de07c8e90c7e5d80c5cc660d72659aaf23f38" + "sha256:1bb436508a7487ac6cb097ae7a7fe5413aefca610550baf58f0940e51ecfb261", + "sha256:3d633e78149eb83b60a07dfabb35579c29aac2d24bb803c18b26fb2ab1a584db" ], - "version": "==1.4.3" + "version": "==1.4.4" }, "webob": { "hashes": [ @@ -876,11 +900,11 @@ }, "xlsxwriter": { "hashes": [ - "sha256:488e1988ab16ff3a9cd58c7656d0a58f8abe46ee58b98eecea78c022db28656b", - "sha256:97ab487b81534415c5313154203f3e8a637d792b1e6a8201e8f7f71da0203c2a" + "sha256:828b3285fc95105f5b1946a6a015b31cf388bd5378fdc6604e4d1b7839df2e77", + "sha256:82a3b0e73e3913483da23791d1a25e4d2dbb3837d1be4129473526b9a270a5cc" ], "index": "pypi", - "version": "==1.2.8" + "version": "==1.2.9" }, "zipp": { "hashes": [ @@ -900,11 +924,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", - "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" + "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545", + "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958" ], "markers": "python_version < '3.8'", - "version": "==1.6.0" + "version": "==1.6.1" }, "more-itertools": { "hashes": [ @@ -951,11 +975,11 @@ }, "pytest": { "hashes": [ - "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3", - "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698" + "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1", + "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8" ], "index": "pypi", - "version": "==5.4.2" + "version": "==5.4.3" }, "six": { "hashes": [ @@ -966,10 +990,10 @@ }, "wcwidth": { "hashes": [ - "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", - "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" + "sha256:79375666b9954d4a1a10739315816324c3e73110af9d0e102d906fdb0aec009f", + "sha256:8c6b5b6ee1360b842645f336d9e5d68c55817c26d3050f46b235ef2bc650e48f" ], - "version": "==0.1.9" + "version": "==0.2.4" }, "zipp": { "hashes": [ diff --git a/config/default.py b/config/default.py index e368b32d..93e4a933 100644 --- a/config/default.py +++ b/config/default.py @@ -9,9 +9,13 @@ JSON_SORT_KEYS = False # CRITICAL. Do not sort the data when returning values NAME = "CR Connect Workflow" FLASK_PORT = environ.get('PORT0') or environ.get('FLASK_PORT', default="5000") CORS_ALLOW_ORIGINS = re.split(r',\s*', environ.get('CORS_ALLOW_ORIGINS', default="localhost:4200, localhost:5002")) -DEVELOPMENT = environ.get('DEVELOPMENT', default="true") == "true" TESTING = environ.get('TESTING', default="false") == "true" -PRODUCTION = (environ.get('PRODUCTION', default="false") == "true") or (not DEVELOPMENT and not TESTING) +PRODUCTION = (environ.get('PRODUCTION', default="false") == "true") +TEST_UID = environ.get('TEST_UID', default="dhf8r") +ADMIN_UIDS = re.split(r',\s*', environ.get('ADMIN_UIDS', default="dhf8r,ajl2j,cah13us,cl3wf")) + +# Sentry flag +ENABLE_SENTRY = environ.get('ENABLE_SENTRY', default="false") == "true" # Add trailing slash to base path APPLICATION_ROOT = re.sub(r'//', '/', '/%s/' % environ.get('APPLICATION_ROOT', default="/").strip('/')) @@ -25,7 +29,7 @@ SQLALCHEMY_DATABASE_URI = environ.get( 'SQLALCHEMY_DATABASE_URI', default="postgresql://%s:%s@%s:%s/%s" % (DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME) ) -TOKEN_AUTH_TTL_HOURS = int(environ.get('TOKEN_AUTH_TTL_HOURS', default=4)) +TOKEN_AUTH_TTL_HOURS = float(environ.get('TOKEN_AUTH_TTL_HOURS', default=24)) TOKEN_AUTH_SECRET_KEY = environ.get('TOKEN_AUTH_SECRET_KEY', default="Shhhh!!! This is secret! And better darn well not show up in prod.") FRONTEND_AUTH_CALLBACK = environ.get('FRONTEND_AUTH_CALLBACK', default="http://localhost:4200/session") SWAGGER_AUTH_KEY = environ.get('SWAGGER_AUTH_KEY', default="SWAGGER") @@ -39,6 +43,14 @@ PB_REQUIRED_DOCS_URL = environ.get('PB_REQUIRED_DOCS_URL', default=PB_BASE_URL + PB_STUDY_DETAILS_URL = environ.get('PB_STUDY_DETAILS_URL', default=PB_BASE_URL + "study?studyid=%i") LDAP_URL = environ.get('LDAP_URL', default="ldap.virginia.edu").strip('/') # No trailing slash or http:// -LDAP_TIMEOUT_SEC = int(environ.get('LDAP_TIMEOUT_SEC', default=3)) - +LDAP_TIMEOUT_SEC = int(environ.get('LDAP_TIMEOUT_SEC', default=1)) +# Email configuration +FALLBACK_EMAILS = ['askresearch@virginia.edu', 'sartographysupport@googlegroups.com'] +MAIL_DEBUG = environ.get('MAIL_DEBUG', default=True) +MAIL_SERVER = environ.get('MAIL_SERVER', default='smtp.mailtrap.io') +MAIL_PORT = environ.get('MAIL_PORT', default=2525) +MAIL_USE_SSL = environ.get('MAIL_USE_SSL', default=False) +MAIL_USE_TLS = environ.get('MAIL_USE_TLS', default=True) +MAIL_USERNAME = environ.get('MAIL_USERNAME', default='') +MAIL_PASSWORD = environ.get('MAIL_PASSWORD', default='') diff --git a/config/testing.py b/config/testing.py index a7c6a893..546ea829 100644 --- a/config/testing.py +++ b/config/testing.py @@ -4,7 +4,6 @@ from os import environ basedir = os.path.abspath(os.path.dirname(__file__)) NAME = "CR Connect Workflow" -DEVELOPMENT = True TESTING = True TOKEN_AUTH_SECRET_KEY = "Shhhh!!! This is secret! And better darn well not show up in prod." PB_ENABLED = False @@ -23,8 +22,8 @@ SQLALCHEMY_DATABASE_URI = environ.get( 'SQLALCHEMY_DATABASE_URI', default="postgresql://%s:%s@%s:%s/%s" % (DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME) ) +ADMIN_UIDS = ['dhf8r'] print('### USING TESTING CONFIG: ###') print('SQLALCHEMY_DATABASE_URI = ', SQLALCHEMY_DATABASE_URI) -print('DEVELOPMENT = ', DEVELOPMENT) print('TESTING = ', TESTING) diff --git a/config/travis-testing.py b/config/travis-testing.py index 17a4b914..8949061a 100644 --- a/config/travis-testing.py +++ b/config/travis-testing.py @@ -2,7 +2,6 @@ import os basedir = os.path.abspath(os.path.dirname(__file__)) NAME = "CR Connect Workflow" -DEVELOPMENT = True TESTING = True SQLALCHEMY_DATABASE_URI = "postgresql://postgres:@localhost:5432/crc_test" TOKEN_AUTH_TTL_HOURS = 2 @@ -12,6 +11,5 @@ PB_ENABLED = False print('+++ USING TRAVIS TESTING CONFIG: +++') print('SQLALCHEMY_DATABASE_URI = ', SQLALCHEMY_DATABASE_URI) -print('DEVELOPMENT = ', DEVELOPMENT) print('TESTING = ', TESTING) print('FRONTEND_AUTH_CALLBACK = ', FRONTEND_AUTH_CALLBACK) diff --git a/crc/__init__.py b/crc/__init__.py index fe510daf..1ac2678f 100644 --- a/crc/__init__.py +++ b/crc/__init__.py @@ -1,11 +1,15 @@ import logging import os +import sentry_sdk import connexion +from jinja2 import Environment, FileSystemLoader from flask_cors import CORS from flask_marshmallow import Marshmallow +from flask_mail import Mail from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy +from sentry_sdk.integrations.flask import FlaskIntegration logging.basicConfig(level=logging.INFO) @@ -40,16 +44,29 @@ connexion_app.add_api('api.yml', base_path='/v1.0') origins_re = [r"^https?:\/\/%s(.*)" % o.replace('.', '\.') for o in app.config['CORS_ALLOW_ORIGINS']] cors = CORS(connexion_app.app, origins=origins_re) +if app.config['ENABLE_SENTRY']: + sentry_sdk.init( + dsn="https://25342ca4e2d443c6a5c49707d68e9f40@o401361.ingest.sentry.io/5260915", + integrations=[FlaskIntegration()] + ) + +# Jinja environment definition, used to render mail templates +template_dir = os.getcwd() + '/crc/static/templates/mails' +env = Environment(loader=FileSystemLoader(template_dir)) +# Mail settings +mail = Mail(app) + print('=== USING THESE CONFIG SETTINGS: ===') -print('DB_HOST = ', ) -print('CORS_ALLOW_ORIGINS = ', app.config['CORS_ALLOW_ORIGINS']) -print('DEVELOPMENT = ', app.config['DEVELOPMENT']) -print('TESTING = ', app.config['TESTING']) -print('PRODUCTION = ', app.config['PRODUCTION']) -print('PB_BASE_URL = ', app.config['PB_BASE_URL']) -print('LDAP_URL = ', app.config['LDAP_URL']) print('APPLICATION_ROOT = ', app.config['APPLICATION_ROOT']) +print('CORS_ALLOW_ORIGINS = ', app.config['CORS_ALLOW_ORIGINS']) +print('DB_HOST = ', app.config['DB_HOST']) +print('LDAP_URL = ', app.config['LDAP_URL']) +print('PB_BASE_URL = ', app.config['PB_BASE_URL']) print('PB_ENABLED = ', app.config['PB_ENABLED']) +print('PRODUCTION = ', app.config['PRODUCTION']) +print('TESTING = ', app.config['TESTING']) +print('TEST_UID = ', app.config['TEST_UID']) +print('ADMIN_UIDS = ', app.config['ADMIN_UIDS']) @app.cli.command() def load_example_data(): @@ -65,3 +82,9 @@ def load_example_rrt_data(): from example_data import ExampleDataLoader ExampleDataLoader.clean_db() ExampleDataLoader().load_rrt() + +@app.cli.command() +def clear_db(): + """Load example data into the database.""" + from example_data import ExampleDataLoader + ExampleDataLoader.clean_db() diff --git a/crc/api.yml b/crc/api.yml index edc3861b..64f6086a 100644 --- a/crc/api.yml +++ b/crc/api.yml @@ -9,54 +9,18 @@ servers: security: - jwt: ['secret'] paths: - /sso_backdoor: + /login: get: - operationId: crc.api.user.backdoor - summary: A backdoor that allows someone to log in as a specific user, if they - are in a staging environment. + operationId: crc.api.user.login + summary: In production, logs the user in via SSO. If not in production, logs in as a specific user for testing. security: [] # Disable security for this endpoint only. parameters: - name: uid - in: query - required: true - schema: - type: string - - name: email_address in: query required: false schema: type: string - - name: display_name - in: query - required: false - schema: - type: string - - name: affiliation - in: query - required: false - schema: - type: string - - name: eppn - in: query - required: false - schema: - type: string - - name: first_name - in: query - required: false - schema: - type: string - - name: last_name - in: query - required: false - schema: - type: string - - name: title - in: query - required: false - schema: - type: string - - name: redirect + - name: redirect_url in: query required: false schema: @@ -150,6 +114,8 @@ paths: $ref: "#/components/schemas/Study" delete: operationId: crc.api.study.delete_study + security: + - auth_admin: ['secret'] summary: Removes the given study completely. tags: - Studies @@ -173,6 +139,30 @@ paths: application/json: schema: $ref: "#/components/schemas/Study" + /study/{study_id}/approvals: + parameters: + - name: study_id + in: path + required: true + description: The id of the study for which workflows should be returned. + schema: + type: integer + format: int32 + get: + operationId: crc.api.approval.get_approvals_for_study + summary: Returns approvals for a single study + tags: + - Studies + - Approvals + responses: + '200': + description: An array of approvals + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Approval" /workflow-specification: get: operationId: crc.api.workflow.all_specifications @@ -227,6 +217,8 @@ paths: $ref: "#/components/schemas/WorkflowSpec" put: operationId: crc.api.workflow.update_workflow_specification + security: + - auth_admin: ['secret'] summary: Modifies an existing workflow specification with the given parameters. tags: - Workflow Specifications @@ -244,6 +236,8 @@ paths: $ref: "#/components/schemas/WorkflowSpec" delete: operationId: crc.api.workflow.delete_workflow_specification + security: + - auth_admin: ['secret'] summary: Removes an existing workflow specification tags: - Workflow Specifications @@ -289,6 +283,8 @@ paths: $ref: "#/components/schemas/WorkflowSpecCategory" post: operationId: crc.api.workflow.add_workflow_spec_category + security: + - auth_admin: ['secret'] summary: Creates a new workflow spec category with the given parameters. tags: - Workflow Specification Category @@ -326,6 +322,8 @@ paths: $ref: "#/components/schemas/WorkflowSpecCategory" put: operationId: crc.api.workflow.update_workflow_spec_category + security: + - auth_admin: ['secret'] summary: Modifies an existing workflow spec category with the given parameters. tags: - Workflow Specification Category @@ -343,6 +341,8 @@ paths: $ref: "#/components/schemas/WorkflowSpecCategory" delete: operationId: crc.api.workflow.delete_workflow_spec_category + security: + - auth_admin: ['secret'] summary: Removes an existing workflow spec category tags: - Workflow Specification Category @@ -444,7 +444,7 @@ paths: $ref: "#/components/schemas/File" delete: operationId: crc.api.file.delete_file - summary: Removes an existing file + summary: Removes an existing file. In the event the file can not be deleted, it is marked as "archived" in the database and is no longer returned unless specifically requested by id. tags: - Files responses: @@ -542,6 +542,8 @@ paths: example: '' put: operationId: crc.api.file.set_reference_file + security: + - auth_admin: ['secret'] summary: Update the contents of a named reference file. tags: - Files @@ -600,6 +602,8 @@ paths: $ref: "#/components/schemas/Workflow" delete: operationId: crc.api.workflow.delete_workflow + security: + - auth_admin: ['secret'] summary: Removes an existing workflow tags: - Workflows and Tasks @@ -738,6 +742,26 @@ paths: text/plain: schema: type: string + /send_email: + parameters: + - name: address + in: query + required: true + description: The address to send a test email to. + schema: + type: string + get: + operationId: crc.api.tools.send_email + summary: Sends an email so we can see if things work or not. + tags: + - Configurator Tools + responses: + '201': + description: Returns any error messages that might come back from sending the email. + content: + text/plain: + schema: + type: string /render_docx: put: operationId: crc.api.tools.render_docx @@ -782,12 +806,62 @@ paths: type: array items: $ref: "#/components/schemas/Script" - /approval: + /approval-counts: parameters: - - name: approver_uid + - name: as_user in: query required: false - description: Restrict results to a given approver uid, maybe we restrict the use of this at somepoint. + description: If provided, returns the approval counts for that user. + schema: + type: string + get: + operationId: crc.api.approval.get_approval_counts + summary: Provides counts for approvals by status for the given user, or all users if no user is provided + tags: + - Approvals + responses: + '200': + description: An dictionary of Approval Statuses and the counts for each + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ApprovalCounts" + /all_approvals: + parameters: + - name: status + in: query + required: false + description: If set to true, returns all the approvals with any status. Defaults to false, leaving out canceled approvals. + schema: + type: boolean + get: + operationId: crc.api.approval.get_all_approvals + summary: Provides a list of all workflows approvals + tags: + - Approvals + responses: + '200': + description: An array of approvals + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Approval" + /approval: + parameters: + - name: status + in: query + required: false + description: If provided, returns just approvals for the given status. + schema: + type: string + - name: as_user + in: query + required: false + description: If provided, returns the approval results as they would appear for that user. schema: type: string get: @@ -830,6 +904,19 @@ paths: application/json: schema: $ref: "#/components/schemas/Approval" + /approval/csv: + get: + operationId: crc.api.approval.get_csv + summary: Provides a list of all users for all approved studies + tags: + - Approvals + responses: + '200': + description: An array of approvals + content: + application/json: + schema: + type: object components: securitySchemes: jwt: @@ -837,6 +924,11 @@ components: scheme: bearer bearerFormat: JWT x-bearerInfoFunc: crc.api.user.verify_token + auth_admin: + type: http + scheme: bearer + bearerFormat: JWT + x-bearerInfoFunc: crc.api.user.verify_token_admin schemas: User: properties: @@ -1243,4 +1335,26 @@ components: type: number format: integer example: 5 + ApprovalCounts: + properties: + PENDING: + type: number + format: integer + example: 5 + APPROVED: + type: number + format: integer + example: 5 + DECLINED: + type: number + format: integer + example: 5 + CANCELED: + type: number + format: integer + example: 5 + AWAITING: + type: number + format: integer + example: 5 diff --git a/crc/api/approval.py b/crc/api/approval.py index 739773c1..b3ee0fed 100644 --- a/crc/api/approval.py +++ b/crc/api/approval.py @@ -1,19 +1,161 @@ -from crc import app, db, session +import json +import pickle +from base64 import b64decode +from datetime import datetime -from crc.api.common import ApiError, ApiErrorSchema -from crc.models.approval import Approval, ApprovalModel, ApprovalSchema +from flask import g + +from crc import db, session +from crc.api.common import ApiError +from crc.models.approval import Approval, ApprovalModel, ApprovalSchema, ApprovalStatus +from crc.models.workflow import WorkflowModel from crc.services.approval_service import ApprovalService +from crc.services.ldap_service import LdapService -def get_approvals(approver_uid = None): - if not approver_uid: - db_approvals = ApprovalService.get_all_approvals() - else: - db_approvals = ApprovalService.get_approvals_per_user(approver_uid) +# Returns counts of approvals in each status group assigned to the given user. +# The goal is to return results as quickly as possible. +def get_approval_counts(as_user=None): + uid = as_user or g.user.uid + + db_user_approvals = db.session.query(ApprovalModel)\ + .filter_by(approver_uid=uid)\ + .filter(ApprovalModel.status != ApprovalStatus.CANCELED.name)\ + .all() + + study_ids = [a.study_id for a in db_user_approvals] + + db_other_approvals = db.session.query(ApprovalModel)\ + .filter(ApprovalModel.study_id.in_(study_ids))\ + .filter(ApprovalModel.approver_uid != uid)\ + .filter(ApprovalModel.status != ApprovalStatus.CANCELED.name)\ + .all() + + # Make a dict of the other approvals where the key is the study id and the value is the approval + # TODO: This won't work if there are more than 2 approvals with the same study_id + other_approvals = {} + for approval in db_other_approvals: + other_approvals[approval.study_id] = approval + + counts = {} + for name, value in ApprovalStatus.__members__.items(): + counts[name] = 0 + + for approval in db_user_approvals: + # Check if another approval has the same study id + if approval.study_id in other_approvals: + other_approval = other_approvals[approval.study_id] + + # Other approval takes precedence over this one + if other_approval.id < approval.id: + if other_approval.status == ApprovalStatus.PENDING.name: + counts[ApprovalStatus.AWAITING.name] += 1 + elif other_approval.status == ApprovalStatus.DECLINED.name: + counts[ApprovalStatus.DECLINED.name] += 1 + elif other_approval.status == ApprovalStatus.CANCELED.name: + counts[ApprovalStatus.CANCELED.name] += 1 + elif other_approval.status == ApprovalStatus.APPROVED.name: + counts[approval.status] += 1 + else: + counts[approval.status] += 1 + else: + counts[approval.status] += 1 + + return counts + + +def get_all_approvals(status=None): + approvals = ApprovalService.get_all_approvals(include_cancelled=status is True) + results = ApprovalSchema(many=True).dump(approvals) + return results + + +def get_approvals(status=None, as_user=None): + #status = ApprovalStatus.PENDING.value + user = g.user.uid + if as_user: + user = as_user + approvals = ApprovalService.get_approvals_per_user(user, status, + include_cancelled=False) + results = ApprovalSchema(many=True).dump(approvals) + return results + + +def get_approvals_for_study(study_id=None): + db_approvals = ApprovalService.get_approvals_for_study(study_id) approvals = [Approval.from_model(approval_model) for approval_model in db_approvals] results = ApprovalSchema(many=True).dump(approvals) return results + +# ----- Begin descent into madness ---- # +def get_csv(): + """A damn lie, it's a json file. A huge bit of a one-off for RRT, but 3 weeks of midnight work can convince a + man to do just about anything""" + approvals = ApprovalService.get_all_approvals(include_cancelled=False) + output = [] + errors = [] + for approval in approvals: + try: + if approval.status != ApprovalStatus.APPROVED.value: + continue + for related_approval in approval.related_approvals: + if related_approval.status != ApprovalStatus.APPROVED.value: + continue + workflow = db.session.query(WorkflowModel).filter(WorkflowModel.id == approval.workflow_id).first() + data = json.loads(workflow.bpmn_workflow_json) + last_task = find_task(data['last_task']['__uuid__'], data['task_tree']) + personnel = extract_value(last_task, 'personnel') + training_val = extract_value(last_task, 'RequiredTraining') + pi_supervisor = extract_value(last_task, 'PISupervisor')['value'] + review_complete = 'AllRequiredTraining' in training_val + pi_uid = workflow.study.primary_investigator_id + pi_details = LdapService.user_info(pi_uid) + details = [] + details.append(pi_details) + for person in personnel: + uid = person['PersonnelComputingID']['value'] + details.append(LdapService.user_info(uid)) + + for person in details: + record = { + "study_id": approval.study_id, + "pi_uid": pi_details.uid, + "pi": pi_details.display_name, + "name": person.display_name, + "uid": person.uid, + "email": person.email_address, + "supervisor": "", + "review_complete": review_complete, + } + # We only know the PI's supervisor. + if person.uid == pi_details.uid: + record["supervisor"] = pi_supervisor + + output.append(record) + + except Exception as e: + errors.append("Error pulling data for workflow #%i: %s" % (approval.workflow_id, str(e))) + return {"results": output, "errors": errors } + + +def extract_value(task, key): + if key in task['data']: + return pickle.loads(b64decode(task['data'][key]['__bytes__'])) + else: + return "" + + +def find_task(uuid, task): + if task['id']['__uuid__'] == uuid: + return task + for child in task['children']: + task = find_task(uuid, child) + if task: + return task +# ----- come back to the world of the living ---- # + + def update_approval(approval_id, body): if approval_id is None: raise ApiError('unknown_approval', 'Please provide a valid Approval ID.') @@ -22,9 +164,18 @@ def update_approval(approval_id, body): if approval_model is None: raise ApiError('unknown_approval', 'The approval "' + str(approval_id) + '" is not recognized.') - approval: Approval = ApprovalSchema().load(body) - approval.update_model(approval_model) + if approval_model.approver_uid != g.user.uid: + raise ApiError("not_your_approval", "You may not modify this approval. It belongs to another user.") + + approval_model.status = body['status'] + approval_model.message = body['message'] + approval_model.date_approved = datetime.now() + session.add(approval_model) session.commit() - result = ApprovalSchema().dump(approval) + # Called only to send emails + approver = body['approver']['uid'] + ApprovalService.update_approval(approval_id, approver) + + result = ApprovalSchema().dump(approval_model) return result diff --git a/crc/api/common.py b/crc/api/common.py index 2cd09522..f8673a5b 100644 --- a/crc/api/common.py +++ b/crc/api/common.py @@ -1,9 +1,12 @@ +from SpiffWorkflow import WorkflowException +from SpiffWorkflow.exceptions import WorkflowTaskExecException + from crc import ma, app class ApiError(Exception): def __init__(self, code, message, status_code=400, - file_name="", task_id="", task_name="", tag=""): + file_name="", task_id="", task_name="", tag="", task_data = {}): self.status_code = status_code self.code = code # a short consistent string describing the error. self.message = message # A detailed message that provides more information. @@ -11,6 +14,7 @@ class ApiError(Exception): self.task_name = task_name or "" # OPTIONAL: The name of the task in the BPMN Diagram. self.file_name = file_name or "" # OPTIONAL: The file that caused the error. self.tag = tag or "" # OPTIONAL: The XML Tag that caused the issue. + self.task_data = task_data or "" # OPTIONAL: A snapshot of data connected to the task when error ocurred. Exception.__init__(self, self.message) @classmethod @@ -20,6 +24,7 @@ class ApiError(Exception): instance.task_id = task.task_spec.name or "" instance.task_name = task.task_spec.description or "" instance.file_name = task.workflow.spec.file or "" + instance.task_data = task.data return instance @classmethod @@ -32,10 +37,21 @@ class ApiError(Exception): instance.file_name = task_spec._wf_spec.file return instance + @classmethod + def from_workflow_exception(cls, code, message, exp: WorkflowException): + """We catch a lot of workflow exception errors, + so consolidating the code, and doing the best things + we can with the data we have.""" + if isinstance(exp, WorkflowTaskExecException): + return ApiError.from_task(code, message, exp.task) + else: + return ApiError.from_task_spec(code, message, exp.sender) + class ApiErrorSchema(ma.Schema): class Meta: - fields = ("code", "message", "workflow_name", "file_name", "task_name", "task_id") + fields = ("code", "message", "workflow_name", "file_name", "task_name", "task_id", + "task_data") @app.errorhandler(ApiError) diff --git a/crc/api/file.py b/crc/api/file.py index 07ced388..5cf54221 100644 --- a/crc/api/file.py +++ b/crc/api/file.py @@ -12,8 +12,9 @@ from crc.services.file_service import FileService def to_file_api(file_model): - """Converts a FileModel object to something we can return via the aip""" - return File.from_models(file_model, FileService.get_file_data(file_model.id)) + """Converts a FileModel object to something we can return via the api""" + return File.from_models(file_model, FileService.get_file_data(file_model.id), + FileService.get_doc_dictionary()) def get_files(workflow_spec_id=None, workflow_id=None, form_field_key=None): @@ -121,7 +122,7 @@ def get_file_info(file_id): def update_file_info(file_id, body): if file_id is None: - raise ApiError('unknown_file', 'Please provide a valid File ID.') + raise ApiError('no_such_file', 'Please provide a valid File ID.') file_model = session.query(FileModel).filter_by(id=file_id).first() diff --git a/crc/api/study.py b/crc/api/study.py index 423f6fe2..8fdd1b4a 100644 --- a/crc/api/study.py +++ b/crc/api/study.py @@ -48,12 +48,10 @@ def update_study(study_id, body): def get_study(study_id): - study_service = StudyService() - study = study_service.get_study(study_id) + study = StudyService.get_study(study_id) if (study is None): - raise ApiError("Study not found", status_code=404) - schema = StudySchema() - return schema.dump(study) + raise ApiError("unknown_study", 'The study "' + study_id + '" is not recognized.', status_code=404) + return StudySchema().dump(study) def delete_study(study_id): diff --git a/crc/api/tools.py b/crc/api/tools.py index 6fb31b71..d140e962 100644 --- a/crc/api/tools.py +++ b/crc/api/tools.py @@ -9,6 +9,8 @@ from crc.api.common import ApiError from crc.scripts.complete_template import CompleteTemplate from crc.scripts.script import Script import crc.scripts +from crc.services.mails import send_test_email + def render_markdown(data, template): """ @@ -20,9 +22,9 @@ def render_markdown(data, template): data = json.loads(data) return template.render(**data) except UndefinedError as ue: - raise ApiError(code="undefined field", message=ue.message) + raise ApiError(code="undefined_field", message=ue.message) except Exception as e: - raise ApiError(code="invalid", message=str(e)) + raise ApiError(code="invalid_render", message=str(e)) def render_docx(): @@ -42,9 +44,9 @@ def render_docx(): cache_timeout=-1 # Don't cache these files on the browser. ) except ValueError as e: - raise ApiError(code="invalid", message=str(e)) + raise ApiError(code="undefined_field", message=str(e)) except Exception as e: - raise ApiError(code="invalid", message=str(e)) + raise ApiError(code="invalid_render", message=str(e)) def list_scripts(): @@ -59,3 +61,8 @@ def list_scripts(): }) return script_meta +def send_email(address): + """Just sends a quick test email to assure the system is working.""" + if not address: + address = "dan@sartography.com" + return send_test_email(address, [address]) \ No newline at end of file diff --git a/crc/api/user.py b/crc/api/user.py index afa2e894..a298808d 100644 --- a/crc/api/user.py +++ b/crc/api/user.py @@ -1,41 +1,122 @@ -import json - -import connexion import flask -from flask import redirect, g, request +from flask import g, request from crc import app, db from crc.api.common import ApiError from crc.models.user import UserModel, UserModelSchema -from crc.services.ldap_service import LdapService, LdapUserInfo +from crc.services.ldap_service import LdapService, LdapModel """ .. module:: crc.api.user :synopsis: Single Sign On (SSO) user login and session handlers """ -def verify_token(token): - failure_error = ApiError("invalid_token", "Unable to decode the token you provided. Please re-authenticate", status_code=403) - if (not 'PRODUCTION' in app.config or not app.config['PRODUCTION']) and token == app.config["SWAGGER_AUTH_KEY"]: + + +def verify_token(token=None): + """ + Verifies the token for the user (if provided). If in production environment and token is not provided, + gets user from the SSO headers and returns their token. + + Args: + token: Optional[str] + + Returns: + token: str + + Raises: + ApiError. If not on production and token is not valid, returns an 'invalid_token' 403 error. + If on production and user is not authenticated, returns a 'no_user' 403 error. + """ + + failure_error = ApiError("invalid_token", "Unable to decode the token you provided. Please re-authenticate", + status_code=403) + + if not _is_production() and (token is None or 'user' not in g): g.user = UserModel.query.first() token = g.user.encode_auth_token() - try: - token_info = UserModel.decode_auth_token(token) - g.user = UserModel.query.filter_by(uid=token_info['sub']).first() - except: - raise failure_error - if g.user is not None: - return token_info + if token: + try: + token_info = UserModel.decode_auth_token(token) + g.user = UserModel.query.filter_by(uid=token_info['sub']).first() + except: + raise failure_error + if g.user is not None: + return token_info + else: + raise failure_error + + # If there's no token and we're in production, get the user from the SSO headers and return their token + if not token and _is_production(): + uid = _get_request_uid(request) + + if uid is not None: + db_user = UserModel.query.filter_by(uid=uid).first() + + if db_user is not None: + g.user = db_user + token = g.user.encode_auth_token().decode() + token_info = UserModel.decode_auth_token(token) + return token_info + + else: + raise ApiError("no_user", "User not found. Please login via the frontend app before accessing this feature.", + status_code=403) + + +def verify_token_admin(token=None): + """ + Verifies the token for the user (if provided) in non-production environment. If in production environment, + checks that the user is in the list of authorized admins + + Args: + token: Optional[str] + + Returns: + token: str + """ + + # If this is production, check that the user is in the list of admins + if _is_production(): + uid = _get_request_uid(request) + + if uid is not None and uid in app.config['ADMIN_UIDS']: + return verify_token() + + # If we're not in production, just use the normal verify_token method else: - raise failure_error + return verify_token(token) def get_current_user(): return UserModelSchema().dump(g.user) -@app.route('/v1.0/login') -def sso_login(): - # This what I see coming back: + +def login( + uid=None, + redirect_url=None, +): + """ + In non-production environment, provides an endpoint for end-to-end system testing that allows the system + to simulate logging in as a specific user. In production environment, simply logs user in via single-sign-on + (SSO) Shibboleth authentication headers. + + Args: + uid: Optional[str] + redirect_url: Optional[str] + + Returns: + str. If not on production, returns the frontend auth callback URL, with auth token appended. + If on production and user is authenticated via SSO, returns the frontend auth callback URL, + with auth token appended. + + Raises: + ApiError. If on production and user is not authenticated, returns a 404 error. + """ + + # ---------------------------------------- + # Shibboleth Authentication Headers + # ---------------------------------------- # X-Remote-Cn: Daniel Harold Funk (dhf8r) # X-Remote-Sn: Funk # X-Remote-Givenname: Daniel @@ -50,62 +131,52 @@ def sso_login(): # X-Forwarded-Host: dev.crconnect.uvadcos.io # X-Forwarded-Server: dev.crconnect.uvadcos.io # Connection: Keep-Alive - uid = request.headers.get("Uid") - if not uid: - uid = request.headers.get("X-Remote-Uid") - if not uid: - raise ApiError("invalid_sso_credentials", "'Uid' nor 'X-Remote-Uid' were present in the headers: %s" - % str(request.headers)) - redirect = request.args.get('redirect') - app.logger.info("SSO_LOGIN: Full URL: " + request.url) - app.logger.info("SSO_LOGIN: User Id: " + uid) - app.logger.info("SSO_LOGIN: Will try to redirect to : " + str(redirect)) + # If we're in production, override any uid with the uid from the SSO request headers + if _is_production(): + uid = _get_request_uid(request) - ldap_service = LdapService() - info = ldap_service.user_info(uid) + if uid: + app.logger.info("SSO_LOGIN: Full URL: " + request.url) + app.logger.info("SSO_LOGIN: User Id: " + uid) + app.logger.info("SSO_LOGIN: Will try to redirect to : " + str(redirect_url)) + + ldap_info = LdapService().user_info(uid) + + if ldap_info: + return _handle_login(ldap_info, redirect_url) + + raise ApiError('404', 'unknown') - return _handle_login(info, redirect) @app.route('/sso') def sso(): response = "" response += "

Headers

" response += "