From a2a14edbb1a77994ac41a6045fecf93b4a182518 Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Thu, 2 Feb 2017 22:22:14 -0500 Subject: [PATCH 1/3] Move ConciergeClient.convert_ms_object to utils module For 2 reasons. 1. I've got a use case where I want to use convert_ms_object() but I don't have a ConciergeClient. So make it a class method? Well, I thought of that, but ... 2. convert_ms_object() doesn't do anything with its first argument whether it's `self` or `cls` so it can really stand alone, separate from ConciergeClient. Hence new home in utils. --- membersuite_api_client/client.py | 14 ++----- .../subscriptions/services.py | 3 +- membersuite_api_client/tests/test_client.py | 25 ------------ membersuite_api_client/tests/test_utils.py | 40 +++++++++++++++++++ membersuite_api_client/utils.py | 9 +++++ 5 files changed, 54 insertions(+), 37 deletions(-) create mode 100644 membersuite_api_client/tests/test_utils.py create mode 100644 membersuite_api_client/utils.py diff --git a/membersuite_api_client/client.py b/membersuite_api_client/client.py index 96558e3..94047bf 100644 --- a/membersuite_api_client/client.py +++ b/membersuite_api_client/client.py @@ -40,6 +40,8 @@ def get_hashed_signature(self, url): return base64.b64encode(hashed).decode("utf-8") def get_session_id_from_login_result(self, login_result): + # @TODO this should be renamed and moved to utils.py. + # rename to `get_session_id_from_api_response` try: return login_result["header"]["header"]["SessionId"] except TypeError: @@ -62,7 +64,7 @@ def request_session(self): if not self.session_id: raise MembersuiteLoginError( - result["body"]["LoginResult"]["Errors"]) + result["body"]["WhoAmIResult"]["Errors"]) return self.session_id @@ -135,16 +137,6 @@ def query_orgs(self, parameters=None, since_when=None): else: return None - def convert_ms_object(self, ms_object): - """ - Converts the list of dictionaries with keys "key" and "value" into - more logical value-key pairs in a plain dictionary. - """ - out_dict = {} - for item in ms_object: - out_dict[item["Key"]] = item["Value"] - return out_dict - def runSQL(self, query, start_record=0): concierge_request_header = self.construct_concierge_header( url="http://membersuite.com/contracts/" diff --git a/membersuite_api_client/subscriptions/services.py b/membersuite_api_client/subscriptions/services.py index eb34ef2..c1ba699 100644 --- a/membersuite_api_client/subscriptions/services.py +++ b/membersuite_api_client/subscriptions/services.py @@ -17,6 +17,7 @@ """ from .models import Subscription +from ..utils import convert_ms_object class SubscriptionService(object): @@ -48,7 +49,7 @@ def get_org_subscriptions(self, org_id, publication_id=None): subscription_list = [] for obj in objects: - sane_obj = self.client.convert_ms_object( + sane_obj = convert_ms_object( obj['Fields']['KeyValueOfstringanyType']) subscription = Subscription( id=sane_obj['ID'], diff --git a/membersuite_api_client/tests/test_client.py b/membersuite_api_client/tests/test_client.py index cad0967..a5fe579 100644 --- a/membersuite_api_client/tests/test_client.py +++ b/membersuite_api_client/tests/test_client.py @@ -5,8 +5,6 @@ import datetime -MS_USER_ID = os.environ.get("MS_USER_ID", None) -MS_USER_PASS = os.environ.get("MS_USER_PASS", None) MS_ACCESS_KEY = os.environ["MS_ACCESS_KEY"] MS_SECRET_KEY = os.environ["MS_SECRET_KEY"] MS_ASSOCIATION_ID = os.environ["MS_ASSOCIATION_ID"] @@ -84,29 +82,6 @@ def test_query_orgs(self): response = client.query_orgs(parameters, since_when) # self.assertFalse(response) - def test_convert_ms_object(self): - """ - Can we parse the list of dicts for org attributes into a dict? - """ - client = ConciergeClient(access_key=MS_ACCESS_KEY, - secret_key=MS_SECRET_KEY, - association_id=MS_ASSOCIATION_ID) - - # Send a login request to receive a session id - session_id = client.request_session() - self.assertTrue(session_id) - parameters = { - 'Name': 'AASHE Test Campus', - } - response = client.query_orgs(parameters) - self.assertEqual(response[0]["Fields"]["KeyValueOfstringanyType"] - [28]["Value"], - 'AASHE Test Campus') - converted_dict = client.convert_ms_object( - response[0]["Fields"]["KeyValueOfstringanyType"]) - - self.assertEqual(converted_dict["Name"], "AASHE Test Campus") - if __name__ == '__main__': unittest.main() diff --git a/membersuite_api_client/tests/test_utils.py b/membersuite_api_client/tests/test_utils.py new file mode 100644 index 0000000..e3c13bf --- /dev/null +++ b/membersuite_api_client/tests/test_utils.py @@ -0,0 +1,40 @@ +import os +import unittest + +from ..client import ConciergeClient +from ..utils import convert_ms_object + + +MS_ACCESS_KEY = os.environ["MS_ACCESS_KEY"] +MS_SECRET_KEY = os.environ["MS_SECRET_KEY"] +MS_ASSOCIATION_ID = os.environ["MS_ASSOCIATION_ID"] + + +class UtilsTestCase(unittest.TestCase): + + def test_convert_ms_object(self): + """ + Can we parse the list of dicts for org attributes into a dict? + """ + client = ConciergeClient(access_key=MS_ACCESS_KEY, + secret_key=MS_SECRET_KEY, + association_id=MS_ASSOCIATION_ID) + + # Send a login request to receive a session id + session_id = client.request_session() + self.assertTrue(session_id) + parameters = { + 'Name': 'AASHE Test Campus', + } + response = client.query_orgs(parameters) + self.assertEqual(response[0]["Fields"]["KeyValueOfstringanyType"] + [28]["Value"], + 'AASHE Test Campus') + converted_dict = convert_ms_object( + response[0]["Fields"]["KeyValueOfstringanyType"]) + + self.assertEqual(converted_dict["Name"], "AASHE Test Campus") + + +if __name__ == '__main__': + unittest.main() diff --git a/membersuite_api_client/utils.py b/membersuite_api_client/utils.py new file mode 100644 index 0000000..98352b2 --- /dev/null +++ b/membersuite_api_client/utils.py @@ -0,0 +1,9 @@ +def convert_ms_object(ms_object): + """ + Converts the list of dictionaries with keys "key" and "value" into + more logical value-key pairs in a plain dictionary. + """ + out_dict = {} + for item in ms_object: + out_dict[item["Key"]] = item["Value"] + return out_dict From 80b224ffea9dde960d0d763bdd16f50df40f131d Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Thu, 2 Feb 2017 22:48:16 -0500 Subject: [PATCH 2/3] Refactor ConciergeClient.get_session_id_from_login_result() Renamed to `get_session_id` and moved to utils module. --- membersuite_api_client/client.py | 14 ++++---------- membersuite_api_client/tests/test_client.py | 8 ++++---- membersuite_api_client/utils.py | 11 +++++++++++ 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/membersuite_api_client/client.py b/membersuite_api_client/client.py index 94047bf..11353c5 100644 --- a/membersuite_api_client/client.py +++ b/membersuite_api_client/client.py @@ -4,6 +4,9 @@ import base64 import hmac +from .utils import get_session_id + + XHTML_NAMESPACE = "http://membersuite.com/schemas" @@ -39,14 +42,6 @@ def get_hashed_signature(self, url): hashed = hmac.new(secret_b, data_b, sha1).digest() return base64.b64encode(hashed).decode("utf-8") - def get_session_id_from_login_result(self, login_result): - # @TODO this should be renamed and moved to utils.py. - # rename to `get_session_id_from_api_response` - try: - return login_result["header"]["header"]["SessionId"] - except TypeError: - return None - def request_session(self): """ Performs initial request to initialize session and get session id @@ -59,8 +54,7 @@ def request_session(self): result = self.client.service.WhoAmI( _soapheaders=[concierge_request_header]) - self.session_id = self.get_session_id_from_login_result( - login_result=result) + self.session_id = get_session_id(result=result) if not self.session_id: raise MembersuiteLoginError( diff --git a/membersuite_api_client/tests/test_client.py b/membersuite_api_client/tests/test_client.py index a5fe579..5d2d9ed 100644 --- a/membersuite_api_client/tests/test_client.py +++ b/membersuite_api_client/tests/test_client.py @@ -1,8 +1,9 @@ +import datetime import os import unittest from ..client import ConciergeClient -import datetime +from ..utils import get_session_id MS_ACCESS_KEY = os.environ["MS_ACCESS_KEY"] @@ -53,9 +54,8 @@ def test_request_session(self): # Check that the session ID in the response headers matches the # previously obtained session, so the user was not re-authenticated # but properly used the established session. - self.assertEqual( - client.get_session_id_from_login_result(login_result=response), - client.session_id) + self.assertEqual(get_session_id(result=response), + client.session_id) def test_query_orgs(self): """ diff --git a/membersuite_api_client/utils.py b/membersuite_api_client/utils.py index 98352b2..a0c74b5 100644 --- a/membersuite_api_client/utils.py +++ b/membersuite_api_client/utils.py @@ -7,3 +7,14 @@ def convert_ms_object(ms_object): for item in ms_object: out_dict[item["Key"]] = item["Value"] return out_dict + + +def get_session_id(result): + """Returns the Session ID for an API result. + + When there's no Session ID, returns None. + """ + try: + return result["header"]["header"]["SessionId"] + except TypeError: + return None From cb1f14ea29ab0b55ff95386a92ae44fd90b3ca9c Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Fri, 3 Feb 2017 23:19:49 -0500 Subject: [PATCH 3/3] Add security module to membersuite_api_client * models.py defines PortalUser. * services.py defines LoginToPortalError and login_to_portal(). --- membersuite_api_client/security/__init__.py | 0 membersuite_api_client/security/models.py | 24 +++++++++++ membersuite_api_client/security/services.py | 40 +++++++++++++++++++ membersuite_api_client/tests/test_security.py | 31 ++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 membersuite_api_client/security/__init__.py create mode 100644 membersuite_api_client/security/models.py create mode 100644 membersuite_api_client/security/services.py create mode 100644 membersuite_api_client/tests/test_security.py diff --git a/membersuite_api_client/security/__init__.py b/membersuite_api_client/security/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/membersuite_api_client/security/models.py b/membersuite_api_client/security/models.py new file mode 100644 index 0000000..be4d791 --- /dev/null +++ b/membersuite_api_client/security/models.py @@ -0,0 +1,24 @@ +from ..utils import convert_ms_object + + +class PortalUser(object): + + def __init__(self, portal_user, session_id=None): + """Create a PortalUser object from a the Zeep'ed XML representation of + a Membersuite PortalUser object. + + """ + fields = convert_ms_object(portal_user["Fields"] + ["KeyValueOfstringanyType"]) + + self.id = fields["ID"] + self.email_address = fields["EmailAddress"] + self.first_name = fields["FirstName"] + self.last_name = fields["LastName"] + + self.session_id = session_id + + self.extra_data = portal_user + + def get_username(self): + return "_membersuite_id_{}".format(self.id) diff --git a/membersuite_api_client/security/services.py b/membersuite_api_client/security/services.py new file mode 100644 index 0000000..af1cdfc --- /dev/null +++ b/membersuite_api_client/security/services.py @@ -0,0 +1,40 @@ +from .models import PortalUser +from ..utils import get_session_id + + +class LoginToPortalError(Exception): + + def __init__(self, result): + self.result = result + + +def login_to_portal(username, password, client): + """Log `username` into the MemberSuite Portal. + + Returns a PortalUser object if successful, raises + LoginToPortalError if not. + + """ + if not client.session_id: + client.request_session() + + concierge_request_header = client.construct_concierge_header( + url=("http://membersuite.com/contracts/IConciergeAPIService/" + "LoginToPortal")) + + result = client.client.service.LoginToPortal( + _soapheaders=[concierge_request_header], + portalUserName=username, + portalPassword=password) + + login_to_portal_result = result["body"]["LoginToPortalResult"] + + if login_to_portal_result["Success"]: + portal_user = login_to_portal_result["ResultValue"]["PortalUser"] + + session_id = get_session_id(result=result) + + return PortalUser(portal_user=portal_user, + session_id=session_id) + else: + raise LoginToPortalError(result=result) diff --git a/membersuite_api_client/tests/test_security.py b/membersuite_api_client/tests/test_security.py new file mode 100644 index 0000000..c235dda --- /dev/null +++ b/membersuite_api_client/tests/test_security.py @@ -0,0 +1,31 @@ +import os +import unittest + +from ..client import ConciergeClient +from ..security.models import PortalUser +from ..security.services import login_to_portal, LoginToPortalError + + +class SecurityServicesTestCase(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.client = ConciergeClient( + access_key=os.environ["MS_ACCESS_KEY"], + secret_key=os.environ["MS_SECRET_KEY"], + association_id=os.environ["MS_ASSOCIATION_ID"]) + + def test_login_to_portal(self): + """Can we log in to the portal?""" + portal_user = login_to_portal( + username=os.environ["TEST_MS_PORTAL_USER_ID"], + password=os.environ["TEST_MS_PORTAL_USER_PASS"], + client=self.client) + self.assertIsInstance(portal_user, PortalUser) + + def test_login_to_portal_failure(self): + """What happens when we can't log in to the portal?""" + with self.assertRaises(LoginToPortalError): + login_to_portal(username="bo-o-o-gus user ID", + password="wrong password", + client=self.client)