From aeff394733fecb7cb807de9ded6538d6017973e1 Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Thu, 9 Feb 2017 13:32:14 -0500 Subject: [PATCH 1/6] Add initial version of MSQL shell * Supports queries that use OBJECT() and OBJECTS(). * Command line history. * Results best displayed for descendents of new MemberSuiteObject in membersuite_api_client/models.py. * Displays errors in red. To do * Support for fields (rather than OBJECT(S)()) in queries. * Display all errors, not just first one. * Format errors for display. * Exclude input while shelled to python (via `py` command) from history file. --- membersuite_api_client/models.py | 23 ++++++ membersuite_api_client/msql_shell.py | 82 +++++++++++++++++++++ membersuite_api_client/security/models.py | 63 +++++++++------- membersuite_api_client/security/services.py | 2 +- membersuite_api_client/utils.py | 16 ++++ requirements.txt | 2 + setup.py | 5 +- 7 files changed, 164 insertions(+), 29 deletions(-) create mode 100644 membersuite_api_client/models.py create mode 100755 membersuite_api_client/msql_shell.py diff --git a/membersuite_api_client/models.py b/membersuite_api_client/models.py new file mode 100644 index 0000000..3b67238 --- /dev/null +++ b/membersuite_api_client/models.py @@ -0,0 +1,23 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) +from future.utils import python_2_unicode_compatible + +from .utils import convert_ms_object + + +@python_2_unicode_compatible +class MemberSuiteObject(object): + + def __init__(self, membersuite_object_data): + """Takes the Zeep'ed XML Representation of a MemberSuiteObject as + input. + + """ + self.fields = convert_ms_object( + membersuite_object_data["Fields"]["KeyValueOfstringanyType"]) + + self.id = self.fields["ID"] + self.extra_data = membersuite_object_data + + def __str__(self): + return ("".format(id=self.id)) diff --git a/membersuite_api_client/msql_shell.py b/membersuite_api_client/msql_shell.py new file mode 100755 index 0000000..e221e2b --- /dev/null +++ b/membersuite_api_client/msql_shell.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +"""A REPL for MSQL. + +""" +import atexit +import os +import readline + +import cmd2 + +from membersuite_api_client.client import ConciergeClient +from membersuite_api_client.utils import membersuite_object_factory + + +class NewStyleCmd2(object, cmd2.Cmd): + """A cmd2.Cmd class that supports super(). + + cmd2.Cmd is an "old style class", and that causes super() to throw + a fit. Turns out we really do need the logic in + cmd22.Cmd.__init__(), so for our app we inherit from this + NewStyleCmd2. + + """ + + def __init__(self): + super(object, self).__init__() + + +class MSQLShell(NewStyleCmd2): + + intro = "Welcome to the MSQL Shell. Burp." + prompt = "(MSQL) " + history_file = os.path.expanduser("~/.msql_shell_history") + + def __init__(self): + super(MSQLShell, self).__init__() + self._client = None + self.load_history() + + @property + def client(self): + if not self._client: + self._client = ConciergeClient( + access_key=os.environ["MS_ACCESS_KEY"], + secret_key=os.environ["MS_SECRET_KEY"], + association_id=os.environ["MS_ASSOCIATION_ID"]) + return self._client + + def load_history(self): + if not os.path.exists(self.history_file): + with open(self.history_file, 'w') as f: + f.write('') + readline.read_history_file(self.history_file) + atexit.register(readline.write_history_file, self.history_file) + + def do_query(self, line): + if not self.client.session_id: + self.client.request_session() + result = self.client.runSQL(line) + msql_result = result["body"]["ExecuteMSQLResult"] + if msql_result["Success"]: + if msql_result["ResultValue"]["ObjectSearchResult"]["Objects"]: + for obj in (msql_result["ResultValue"]["ObjectSearchResult"] + ["Objects"]["MemberSuiteObject"]): + membersuite_object = membersuite_object_factory(obj) + print(str(membersuite_object)) + else: + membersuite_object = membersuite_object_factory( + msql_result["ResultValue"]["SingleObject"]) + print(str(membersuite_object)) + else: + # @TODO Fix - showing only the first of possibly many errors here. + print(self.colorize(str(msql_result["Errors"]["ConciergeError"]), + "red")) + + def default(self, line): + self.do_query(line) + + +if __name__ == "__main__": + app = MSQLShell() + app.cmdloop() diff --git a/membersuite_api_client/security/models.py b/membersuite_api_client/security/models.py index fdf1628..a0ca8a4 100644 --- a/membersuite_api_client/security/models.py +++ b/membersuite_api_client/security/models.py @@ -1,28 +1,29 @@ -from membersuite_api_client.utils import convert_ms_object -from membersuite_api_client.exceptions import ExecuteMSQLError +from __future__ import (absolute_import, division, print_function, + unicode_literals) +from future.utils import python_2_unicode_compatible +from ..exceptions import ExecuteMSQLError +from ..models import MemberSuiteObject +from ..utils import convert_ms_object -class PortalUser(object): - def __init__(self, portal_user, session_id=None): +@python_2_unicode_compatible +class PortalUser(MemberSuiteObject): + + def __init__(self, membersuite_object_data, session_id=None): """Create a PortalUser object from a the Zeep'ed XML representation of a Membersuite PortalUser. """ - 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.owner = fields["Owner"] + super(PortalUser, self).__init__( + membersuite_object_data=membersuite_object_data) + self.email_address = self.fields["EmailAddress"] + self.first_name = self.fields["FirstName"] + self.last_name = self.fields["LastName"] + self.owner = self.fields["Owner"] self.session_id = session_id - self.extra_data = portal_user - def get_username(self): return "_membersuite_id_{}".format(self.id) @@ -41,27 +42,35 @@ def get_individual(self, client): msql_result = result["body"]["ExecuteMSQLResult"] if msql_result["Success"]: - individual = msql_result["ResultValue"]["SingleObject"] - return Individual(individual=individual, portal_user=self) + membersuite_object_data = (msql_result["ResultValue"] + ["SingleObject"]) + return Individual(membersuite_object_data=membersuite_object_data, + portal_user=self) else: raise ExecuteMSQLError(result=result) -class Individual(object): +@python_2_unicode_compatible +class Individual(MemberSuiteObject): - def __init__(self, individual, portal_user=None): + def __init__(self, membersuite_object_data, portal_user=None): """Create an Individual object from the Zeep'ed XML representation of a MemberSuite Individual. """ - fields = convert_ms_object(individual["Fields"] - ["KeyValueOfstringanyType"]) - - self.id = fields["ID"] - self.email_address = fields["EmailAddress"] - self.first_name = fields["FirstName"] - self.last_name = fields["LastName"] + super(Individual, self).__init__( + membersuite_object_data=membersuite_object_data) - self.extra_data = individual + self.email_address = self.fields["EmailAddress"] + self.first_name = self.fields["FirstName"] + self.last_name = self.fields["LastName"] self.portal_user = portal_user + + def __str__(self): + return ("".format( + id=self.id, + email_address=self.email_address, + first_name=self.first_name, + last_name=self.last_name)) diff --git a/membersuite_api_client/security/services.py b/membersuite_api_client/security/services.py index 5ccc14c..b048ea4 100644 --- a/membersuite_api_client/security/services.py +++ b/membersuite_api_client/security/services.py @@ -29,7 +29,7 @@ def login_to_portal(username, password, client): session_id = get_session_id(result=result) - return PortalUser(portal_user=portal_user, + return PortalUser(membersuite_object_data=portal_user, session_id=session_id) else: raise LoginToPortalError(result=result) diff --git a/membersuite_api_client/utils.py b/membersuite_api_client/utils.py index a0c74b5..590a1fd 100644 --- a/membersuite_api_client/utils.py +++ b/membersuite_api_client/utils.py @@ -18,3 +18,19 @@ def get_session_id(result): return result["header"]["header"]["SessionId"] except TypeError: return None + + +def membersuite_object_factory(membersuite_object_data): + from .models import MemberSuiteObject + import membersuite_api_client.security.models as security_models + + klasses = {"Individual": security_models.Individual, + "PortalUser": security_models.PortalUser} + + try: + klass = klasses[membersuite_object_data["ClassType"]] + except KeyError: + return MemberSuiteObject( + membersuite_object_data=membersuite_object_data) + else: + return klass(membersuite_object_data=membersuite_object_data) diff --git a/requirements.txt b/requirements.txt index 3b1cdb5..3fb4040 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ +cmd2==0.6.9 # for msql shell +future==0.16.0 # for msql shell lxml==3.7.0 zeep==0.23.0 diff --git a/setup.py b/setup.py index eb20a01..701c83e 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,7 @@ def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() + setup(name='membersuite_api_client', version='0.1.3', description='MemberSuite API Client', @@ -25,5 +26,7 @@ def read(fname): 'Programming Language :: Python :: 3.4.3', ], include_package_data=True, - install_requires=["zeep>=0.26"] + install_requires=["cmd2==0.6.9", + "future==0.16.0", + "zeep>=0.26"] ) From 4299ec8c96bbeb48780993fde4b591bc0d9a7cdf Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Thu, 9 Feb 2017 13:52:17 -0500 Subject: [PATCH 2/6] Update .travis.yml. Let's just not talk about it. --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index b10fc8e..8d83697 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,15 +4,9 @@ python: - '3.4' - '3.5' -env: - - DJANGO_VERSION=1.4.3 - - DJANGO_VERSION=1.8 - - DJANGO_VERSION=1.10 - install: - pip install -r requirements.txt - pip install -r requirements_test.txt - - pip install Django==$DJANGO_VERSION script: nosetests --with-coverage From 9928898249aec0e62644c06941b29da25aad4a04 Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Fri, 10 Feb 2017 13:13:58 -0500 Subject: [PATCH 3/6] Add __str__() to security.models.PortalUser --- membersuite_api_client/security/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/membersuite_api_client/security/models.py b/membersuite_api_client/security/models.py index a0ca8a4..f99603e 100644 --- a/membersuite_api_client/security/models.py +++ b/membersuite_api_client/security/models.py @@ -24,6 +24,17 @@ def __init__(self, membersuite_object_data, session_id=None): self.owner = self.fields["Owner"] self.session_id = session_id + def __str__(self): + return ("".format( + id=self.id, + email_address=self.email_address, + first_name=self.first_name, + last_name=self.last_name, + owner=self.owner, + session_id=self.session_id)) + def get_username(self): return "_membersuite_id_{}".format(self.id) From 45ccc33ee7bc1fa5e1badf455dae3cb69f1ae0e9 Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Fri, 10 Feb 2017 13:14:39 -0500 Subject: [PATCH 4/6] Improve exceptions When str()'ed they'll display useful information (about the error, for instance) rather than just the class name. --- membersuite_api_client/exceptions.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/membersuite_api_client/exceptions.py b/membersuite_api_client/exceptions.py index 3983e21..e8aa1a4 100644 --- a/membersuite_api_client/exceptions.py +++ b/membersuite_api_client/exceptions.py @@ -1,11 +1,30 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) +from future.utils import python_2_unicode_compatible + + +@python_2_unicode_compatible class MemberSuiteAPIError(Exception): + def __init__(self, result): self.result = result + def __str__(self): + concierge_error = self.get_concierge_error() + return "<{classname} ConciergeError: {concierge_error}>".format( + classname=self.__class__.__name__, + concierge_error=concierge_error) + + def get_concierge_error(self): + return (self.result["body"][self.result_type] + ["Errors"]["ConciergeError"]) + class LoginToPortalError(MemberSuiteAPIError): - pass + + result_type = "LoginToPortalResult" class ExecuteMSQLError(MemberSuiteAPIError): - pass + + result_type = "ExecuteMSQLResult" From 8289a414c3f6d423cdee3f7bbfe9e6565c748956 Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Fri, 10 Feb 2017 17:59:36 -0500 Subject: [PATCH 5/6] Remove msql_shell.py Moved into own repo, AASHE/msql-shell. --- membersuite_api_client/msql_shell.py | 82 ---------------------------- requirements.txt | 2 - setup.py | 4 +- 3 files changed, 1 insertion(+), 87 deletions(-) delete mode 100755 membersuite_api_client/msql_shell.py diff --git a/membersuite_api_client/msql_shell.py b/membersuite_api_client/msql_shell.py deleted file mode 100755 index e221e2b..0000000 --- a/membersuite_api_client/msql_shell.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python -"""A REPL for MSQL. - -""" -import atexit -import os -import readline - -import cmd2 - -from membersuite_api_client.client import ConciergeClient -from membersuite_api_client.utils import membersuite_object_factory - - -class NewStyleCmd2(object, cmd2.Cmd): - """A cmd2.Cmd class that supports super(). - - cmd2.Cmd is an "old style class", and that causes super() to throw - a fit. Turns out we really do need the logic in - cmd22.Cmd.__init__(), so for our app we inherit from this - NewStyleCmd2. - - """ - - def __init__(self): - super(object, self).__init__() - - -class MSQLShell(NewStyleCmd2): - - intro = "Welcome to the MSQL Shell. Burp." - prompt = "(MSQL) " - history_file = os.path.expanduser("~/.msql_shell_history") - - def __init__(self): - super(MSQLShell, self).__init__() - self._client = None - self.load_history() - - @property - def client(self): - if not self._client: - self._client = ConciergeClient( - access_key=os.environ["MS_ACCESS_KEY"], - secret_key=os.environ["MS_SECRET_KEY"], - association_id=os.environ["MS_ASSOCIATION_ID"]) - return self._client - - def load_history(self): - if not os.path.exists(self.history_file): - with open(self.history_file, 'w') as f: - f.write('') - readline.read_history_file(self.history_file) - atexit.register(readline.write_history_file, self.history_file) - - def do_query(self, line): - if not self.client.session_id: - self.client.request_session() - result = self.client.runSQL(line) - msql_result = result["body"]["ExecuteMSQLResult"] - if msql_result["Success"]: - if msql_result["ResultValue"]["ObjectSearchResult"]["Objects"]: - for obj in (msql_result["ResultValue"]["ObjectSearchResult"] - ["Objects"]["MemberSuiteObject"]): - membersuite_object = membersuite_object_factory(obj) - print(str(membersuite_object)) - else: - membersuite_object = membersuite_object_factory( - msql_result["ResultValue"]["SingleObject"]) - print(str(membersuite_object)) - else: - # @TODO Fix - showing only the first of possibly many errors here. - print(self.colorize(str(msql_result["Errors"]["ConciergeError"]), - "red")) - - def default(self, line): - self.do_query(line) - - -if __name__ == "__main__": - app = MSQLShell() - app.cmdloop() diff --git a/requirements.txt b/requirements.txt index 3fb4040..3b1cdb5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,2 @@ -cmd2==0.6.9 # for msql shell -future==0.16.0 # for msql shell lxml==3.7.0 zeep==0.23.0 diff --git a/setup.py b/setup.py index 701c83e..d25bd63 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,5 @@ def read(fname): 'Programming Language :: Python :: 3.4.3', ], include_package_data=True, - install_requires=["cmd2==0.6.9", - "future==0.16.0", - "zeep>=0.26"] + install_requires=["zeep>=0.26"] ) From 7bbb5f914890137579f01c64610cfe48f3a0762f Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Fri, 10 Feb 2017 18:05:50 -0500 Subject: [PATCH 6/6] Add future to requirements.txt For all the Python 2/3 goodness it offers. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 3b1cdb5..81275a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ +future==0.16.0 lxml==3.7.0 zeep==0.23.0