diff --git a/membersuite_api_client/client.py b/membersuite_api_client/client.py index 96558e3..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,12 +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): - 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 @@ -57,12 +54,11 @@ 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( - result["body"]["LoginResult"]["Errors"]) + result["body"]["WhoAmIResult"]["Errors"]) return self.session_id @@ -135,16 +131,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/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/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..5d2d9ed 100644 --- a/membersuite_api_client/tests/test_client.py +++ b/membersuite_api_client/tests/test_client.py @@ -1,12 +1,11 @@ +import datetime import os import unittest from ..client import ConciergeClient -import datetime +from ..utils import get_session_id -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"] @@ -55,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): """ @@ -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_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) 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..a0c74b5 --- /dev/null +++ b/membersuite_api_client/utils.py @@ -0,0 +1,20 @@ +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 + + +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