From 2d3402a719058dfa563f86f1c074ba9e0fafe09c Mon Sep 17 00:00:00 2001 From: Dan Funk Date: Mon, 20 Apr 2020 15:16:33 -0400 Subject: [PATCH 1/2] Ldap Service with Test and mocks. LDAP_URL can be set in an environment variable. --- Pipfile | 1 + Pipfile.lock | 61 +++++++++++++++++---------- config/default.py | 2 + crc/services/ldap_service.py | 38 +++++++++++++++++ tests/data/ldap_response.json | 79 +++++++++++++++++++++++++++++++++++ tests/test_ldap_service.py | 33 +++++++++++++++ 6 files changed, 191 insertions(+), 23 deletions(-) create mode 100644 crc/services/ldap_service.py create mode 100644 tests/data/ldap_response.json create mode 100644 tests/test_ldap_service.py diff --git a/Pipfile b/Pipfile index 82362b4d..6c9960d2 100644 --- a/Pipfile +++ b/Pipfile @@ -35,6 +35,7 @@ flask-sso = "*" python-dateutil = "*" pandas = "*" xlrd = "*" +ldap3 = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index f24df400..4cba898e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "fd7b3a41fb37479aa670fd23bad0f4770c76cd4e8e75bbf5ddeba452e95e7e94" + "sha256": "26d23456010d3e5a559386d412cef3beacd92d5a4e474f2afdb0737ea0f20f04" }, "pipfile-spec": 6, "requires": { @@ -391,6 +391,14 @@ ], "version": "==4.6.8" }, + "ldap3": { + "hashes": [ + "sha256:17f04298b70bf7ecaa5db8a7d8622b5a962ef7fc2b245b2eea705ac1c24338c0", + "sha256:81df4ac8b6df10fb1f05b17c18d0cb8c4c344d5a03083c382824960ed959cf5b" + ], + "index": "pypi", + "version": "==2.7" + }, "lxml": { "hashes": [ "sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd", @@ -494,29 +502,29 @@ }, "numpy": { "hashes": [ - "sha256:1598a6de323508cfeed6b7cd6c4efb43324f4692e20d1f76e1feec7f59013448", - "sha256:1b0ece94018ae21163d1f651b527156e1f03943b986188dd81bc7e066eae9d1c", - "sha256:2e40be731ad618cb4974d5ba60d373cdf4f1b8dcbf1dcf4d9dff5e212baf69c5", - "sha256:4ba59db1fcc27ea31368af524dcf874d9277f21fd2e1f7f1e2e0c75ee61419ed", - "sha256:59ca9c6592da581a03d42cc4e270732552243dc45e87248aa8d636d53812f6a5", - "sha256:5e0feb76849ca3e83dd396254e47c7dba65b3fa9ed3df67c2556293ae3e16de3", - "sha256:6d205249a0293e62bbb3898c4c2e1ff8a22f98375a34775a259a0523111a8f6c", - "sha256:6fcc5a3990e269f86d388f165a089259893851437b904f422d301cdce4ff25c8", - "sha256:82847f2765835c8e5308f136bc34018d09b49037ec23ecc42b246424c767056b", - "sha256:87902e5c03355335fc5992a74ba0247a70d937f326d852fc613b7f53516c0963", - "sha256:9ab21d1cb156a620d3999dd92f7d1c86824c622873841d6b080ca5495fa10fef", - "sha256:a1baa1dc8ecd88fb2d2a651671a84b9938461e8a8eed13e2f0a812a94084d1fa", - "sha256:a244f7af80dacf21054386539699ce29bcc64796ed9850c99a34b41305630286", - "sha256:a35af656a7ba1d3decdd4fae5322b87277de8ac98b7d9da657d9e212ece76a61", - "sha256:b1fe1a6f3a6f355f6c29789b5927f8bd4f134a4bd9a781099a7c4f66af8850f5", - "sha256:b5ad0adb51b2dee7d0ee75a69e9871e2ddfb061c73ea8bc439376298141f77f5", - "sha256:ba3c7a2814ec8a176bb71f91478293d633c08582119e713a0c5351c0f77698da", - "sha256:cd77d58fb2acf57c1d1ee2835567cd70e6f1835e32090538f17f8a3a99e5e34b", - "sha256:cdb3a70285e8220875e4d2bc394e49b4988bdb1298ffa4e0bd81b2f613be397c", - "sha256:deb529c40c3f1e38d53d5ae6cd077c21f1d49e13afc7936f7f868455e16b64a0", - "sha256:e7894793e6e8540dbeac77c87b489e331947813511108ae097f1715c018b8f3d" + "sha256:0aa2b318cf81eb1693fcfcbb8007e95e231d7e1aa24288137f3b19905736c3ee", + "sha256:163c78c04f47f26ca1b21068cea25ed7c5ecafe5f5ab2ea4895656a750582b56", + "sha256:1e37626bcb8895c4b3873fcfd54e9bfc5ffec8d0f525651d6985fcc5c6b6003c", + "sha256:264fd15590b3f02a1fbc095e7e1f37cdac698ff3829e12ffdcffdce3772f9d44", + "sha256:3d9e1554cd9b5999070c467b18e5ae3ebd7369f02706a8850816f576a954295f", + "sha256:40c24960cd5cec55222963f255858a1c47c6fa50a65a5b03fd7de75e3700eaaa", + "sha256:46f404314dbec78cb342904f9596f25f9b16e7cf304030f1339e553c8e77f51c", + "sha256:4847f0c993298b82fad809ea2916d857d0073dc17b0510fbbced663b3265929d", + "sha256:48e15612a8357393d176638c8f68a19273676877caea983f8baf188bad430379", + "sha256:6725d2797c65598778409aba8cd67077bb089d5b7d3d87c2719b206dc84ec05e", + "sha256:99f0ba97e369f02a21bb95faa3a0de55991fd5f0ece2e30a9e2eaebeac238921", + "sha256:a41f303b3f9157a31ce7203e3ca757a0c40c96669e72d9b6ee1bce8507638970", + "sha256:a4305564e93f5c4584f6758149fd446df39fd1e0a8c89ca0deb3cce56106a027", + "sha256:a551d8cc267c634774830086da42e4ba157fa41dd3b93982bc9501b284b0c689", + "sha256:a6bc9432c2640b008d5f29bad737714eb3e14bb8854878eacf3d7955c4e91c36", + "sha256:c60175d011a2e551a2f74c84e21e7c982489b96b6a5e4b030ecdeacf2914da68", + "sha256:e46e2384209c91996d5ec16744234d1c906ab79a701ce1a26155c9ec890b8dc8", + "sha256:e607b8cdc2ae5d5a63cd1bec30a15b5ed583ac6a39f04b7ba0f03fcfbf29c05b", + "sha256:e94a39d5c40fffe7696009dbd11bc14a349b377e03a384ed011e03d698787dd3", + "sha256:eb2286249ebfe8fcb5b425e5ec77e4736d53ee56d3ad296f8947f67150f495e3", + "sha256:fdee7540d12519865b423af411bd60ddb513d2eb2cd921149b732854995bbf8b" ], - "version": "==1.18.2" + "version": "==1.18.3" }, "openapi-spec-validator": { "hashes": [ @@ -598,6 +606,13 @@ "index": "pypi", "version": "==2.8.5" }, + "pyasn1": { + "hashes": [ + "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" + ], + "version": "==0.4.8" + }, "pycparser": { "hashes": [ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", diff --git a/config/default.py b/config/default.py index d999f3a8..905f3b37 100644 --- a/config/default.py +++ b/config/default.py @@ -42,6 +42,8 @@ PB_INVESTIGATORS_URL = environ.get('PB_INVESTIGATORS_URL', default=PB_BASE_URL + PB_REQUIRED_DOCS_URL = environ.get('PB_REQUIRED_DOCS_URL', default=PB_BASE_URL + "required_docs?studyid=%i") 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") + print('=== USING DEFAULT CONFIG: ===') print('DB_HOST = ', DB_HOST) print('DEVELOPMENT = ', DEVELOPMENT) diff --git a/crc/services/ldap_service.py b/crc/services/ldap_service.py new file mode 100644 index 00000000..2b3cb009 --- /dev/null +++ b/crc/services/ldap_service.py @@ -0,0 +1,38 @@ +from crc import app +from ldap3 import Connection + + +class LdapUserInfo(object): + + def __init__(self, entry): + self.display_name = entry.displayName + self.given_name = ", ".join(entry.givenName) + self.email = entry.mail + self.telephone_number= ", ".join(entry.telephoneNumber) + self.title = ", ".join(entry.title) + self.department = ", ".join(entry.uvaDisplayDepartment) + self.affiliation = ", ".join(entry.uvaPersonIAMAffiliation) + self.sponsor_type = ", ".join(entry.uvaPersonSponsoredType) + + +class LdapService(object): + search_base = "ou=People,o=University of Virginia,c=US" + attributes = ['cn', 'displayName', 'givenName', 'mail', 'objectClass', 'UvaDisplayDepartment', + 'telephoneNumber', 'title', 'uvaPersonIAMAffiliation', 'uvaPersonSponsoredType'] + search_string = "(&(objectclass=person)(uid=%s))" + + def __init__(self, connection=None): + if connection is None: + self.LDAP_URL = app.config['LDAP_URL'] + self.conn = Connection(self.LDAP_URL, auto_bind=True, client_strategy='SYNC') + else: + self.conn = connection + + def __del__(self): + self.conn.unbind() + + def user_info(self, uva_uid): + search_string = LdapService.search_string % uva_uid + self.conn.search(LdapService.search_base, search_string, attributes=LdapService.attributes) + entry = self.conn.entries[0] + return(LdapUserInfo(entry)) diff --git a/tests/data/ldap_response.json b/tests/data/ldap_response.json new file mode 100644 index 00000000..3dc2b4e9 --- /dev/null +++ b/tests/data/ldap_response.json @@ -0,0 +1,79 @@ +{ + "entries": [ + { + "attributes": { + "cn": [ + "Laura Barnes (lb3dp)" + ], + "displayName": "Laura Barnes", + "givenName": [ + "Laura" + ], + "mail": [ + "lb3dp@virginia.edu" + ], + "objectClass": [ + "top", + "person", + "organizationalPerson", + "inetOrgPerson", + "uvaPerson", + "uidObject" + ], + "telephoneNumber": [ + "+1 (434) 924-1723" + ], + "title": [ + "E0:Associate Professor of Systems and Information Engineering" + ], + "uvaDisplayDepartment": [ + "E0:EN-Eng Sys and Environment" + ], + "uvaPersonIAMAffiliation": [ + "faculty" + ], + "uvaPersonSponsoredType": [ + "Staff" + ] + }, + "dn": "uid=lb3dp,ou=People,o=University of Virginia,c=US", + "raw": { + "cn": [ + "Laura Barnes (lb3dp)" + ], + "displayName": [ + "Laura Barnes" + ], + "givenName": [ + "Laura" + ], + "mail": [ + "lb3dp@virginia.edu" + ], + "objectClass": [ + "top", + "person", + "organizationalPerson", + "inetOrgPerson", + "uvaPerson", + "uidObject" + ], + "telephoneNumber": [ + "+1 (434) 924-1723" + ], + "title": [ + "E0:Associate Professor of Systems and Information Engineering" + ], + "uvaDisplayDepartment": [ + "E0:EN-Eng Sys and Environment" + ], + "uvaPersonIAMAffiliation": [ + "faculty" + ], + "uvaPersonSponsoredType": [ + "Staff" + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/test_ldap_service.py b/tests/test_ldap_service.py new file mode 100644 index 00000000..9780e921 --- /dev/null +++ b/tests/test_ldap_service.py @@ -0,0 +1,33 @@ +import os + +from crc import app +from crc.services.ldap_service import LdapService +from tests.base_test import BaseTest +from ldap3 import Server, Connection, ALL, MOCK_SYNC + + +class TestLdapService(BaseTest): + + def setUp(self): + server = Server('my_fake_server') + self.connection = Connection(server, client_strategy=MOCK_SYNC) + file_path = os.path.abspath(os.path.join(app.root_path, '..', 'tests', 'data', 'ldap_response.json')) + self.connection.strategy.entries_from_json(file_path) + self.connection.bind() + self.ldap_service = LdapService(self.connection) + + def tearDown(self): + self.connection.unbind() + + def test_get_single_user(self): + user_info = self.ldap_service.user_info("lb3dp") + self.assertIsNotNone(user_info) + self.assertEqual("Laura Barnes", user_info.display_name) + self.assertEqual("Laura", user_info.given_name) + self.assertEqual("lb3dp@virginia.edu", user_info.email) + self.assertEqual("+1 (434) 924-1723", user_info.telephone_number) + self.assertEqual("E0:Associate Professor of Systems and Information Engineering", user_info.title) + self.assertEqual("E0:EN-Eng Sys and Environment", user_info.department) + self.assertEqual("faculty", user_info.affiliation) + self.assertEqual("Staff", user_info.sponsor_type) + From edbd75bb75e4d0af7f62a4c079f65e5967a2a780 Mon Sep 17 00:00:00 2001 From: Dan Funk Date: Mon, 20 Apr 2020 16:02:13 -0400 Subject: [PATCH 2/2] Connect LDAP Requests to the StudyInfo service so we get back additional details. --- config/default.py | 2 +- crc/scripts/study_info.py | 21 +++++++++++++++++++-- crc/services/ldap_service.py | 23 +++++++++++++++++------ tests/test_ldap_service.py | 7 +++++++ 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/config/default.py b/config/default.py index 905f3b37..079c9451 100644 --- a/config/default.py +++ b/config/default.py @@ -43,7 +43,7 @@ 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") - +LDAP_TIMEOUT_SEC = environ.get('LDAP_TIMEOUT_SEC', default=3) print('=== USING DEFAULT CONFIG: ===') print('DB_HOST = ', DB_HOST) print('DEVELOPMENT = ', DEVELOPMENT) diff --git a/crc/scripts/study_info.py b/crc/scripts/study_info.py index 7950deba..c1f3f5f6 100644 --- a/crc/scripts/study_info.py +++ b/crc/scripts/study_info.py @@ -1,12 +1,16 @@ -from crc import session +from ldap3.core.exceptions import LDAPSocketOpenError + +from crc import session, app from crc.api.common import ApiError from crc.models.study import StudyModel, StudySchema from crc.scripts.script import Script +from crc.services.ldap_service import LdapService from crc.services.protocol_builder import ProtocolBuilderService from crc.services.workflow_processor import WorkflowProcessor class StudyInfo(Script): + """Just your basic class that can pull in data from a few api endpoints and do a basic task.""" pb = ProtocolBuilderService() type_options = ['info', 'investigators', 'details'] @@ -78,5 +82,18 @@ class StudyInfo(Script): """Convert array of investigators from protocol builder into a dictionary keyed on the type""" output = {} for i in pb_investigators: - output[i["INVESTIGATORTYPE"]] = {"user_id": i["NETBADGEID"], "type_full": i["INVESTIGATORTYPEFULL"]} + dict = {"user_id": i["NETBADGEID"], "type_full": i["INVESTIGATORTYPEFULL"]} + dict.update(self.get_ldap_dict_if_available(i["NETBADGEID"])) + output[i["INVESTIGATORTYPE"]] = dict return output + + def get_ldap_dict_if_available(self, user_id): + try: + ldap_service = LdapService() + return ldap_service.user_info(user_id).__dict__ + except ApiError: + app.logger.info(ApiError.message) + return {} + except LDAPSocketOpenError: + app.logger.info("Failed to connect to LDAP Server.") + return {} \ No newline at end of file diff --git a/crc/services/ldap_service.py b/crc/services/ldap_service.py index 2b3cb009..924de455 100644 --- a/crc/services/ldap_service.py +++ b/crc/services/ldap_service.py @@ -1,13 +1,15 @@ from crc import app -from ldap3 import Connection +from ldap3 import Connection, Server + +from crc.api.common import ApiError class LdapUserInfo(object): def __init__(self, entry): - self.display_name = entry.displayName + self.display_name = entry.displayName.value self.given_name = ", ".join(entry.givenName) - self.email = entry.mail + self.email = entry.mail.value self.telephone_number= ", ".join(entry.telephoneNumber) self.title = ", ".join(entry.title) self.department = ", ".join(entry.uvaDisplayDepartment) @@ -15,6 +17,8 @@ class LdapUserInfo(object): self.sponsor_type = ", ".join(entry.uvaPersonSponsoredType) + + class LdapService(object): search_base = "ou=People,o=University of Virginia,c=US" attributes = ['cn', 'displayName', 'givenName', 'mail', 'objectClass', 'UvaDisplayDepartment', @@ -22,17 +26,24 @@ class LdapService(object): search_string = "(&(objectclass=person)(uid=%s))" def __init__(self, connection=None): + self.conn = None if connection is None: - self.LDAP_URL = app.config['LDAP_URL'] - self.conn = Connection(self.LDAP_URL, auto_bind=True, client_strategy='SYNC') + server = Server(app.config['LDAP_URL'], connect_timeout=app.config['LDAP_TIMEOUT_SEC']) + self.conn = Connection(server, + auto_bind=True, + receive_timeout=app.config['LDAP_TIMEOUT_SEC'], + ) else: self.conn = connection def __del__(self): - self.conn.unbind() + if self.conn: + self.conn.unbind() def user_info(self, uva_uid): search_string = LdapService.search_string % uva_uid self.conn.search(LdapService.search_base, search_string, attributes=LdapService.attributes) + if len(self.conn.entries) < 1: + raise ApiError("missing_ldap_record", "Unable to locate a user with id %s in LDAP" % uva_uid) entry = self.conn.entries[0] return(LdapUserInfo(entry)) diff --git a/tests/test_ldap_service.py b/tests/test_ldap_service.py index 9780e921..03a1961f 100644 --- a/tests/test_ldap_service.py +++ b/tests/test_ldap_service.py @@ -1,6 +1,7 @@ import os from crc import app +from crc.api.common import ApiError from crc.services.ldap_service import LdapService from tests.base_test import BaseTest from ldap3 import Server, Connection, ALL, MOCK_SYNC @@ -31,3 +32,9 @@ class TestLdapService(BaseTest): self.assertEqual("faculty", user_info.affiliation) self.assertEqual("Staff", user_info.sponsor_type) + def test_find_missing_user(self): + try: + user_info = self.ldap_service.user_info("nosuch") + self.assertFalse(True, "An API error should be raised.") + except ApiError as ae: + self.assertEquals("missing_ldap_record", ae.code) \ No newline at end of file