From 9fe4ea478d22d7dfb2638eee8a8b2246f90af002 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Mon, 12 Dec 2016 01:43:51 +0100 Subject: [refactor] reorganize API so that whitelisting doesn't have to peek into the data. added more documentation and some tests stubs too. --- docs/core/core_api_contract | 2 + docs/core/index.rst | 228 ++++++++++++++++++++++++++++++++++- src/leap/bitmask/__init__.py | 6 +- src/leap/bitmask/cli/bitmask_cli.py | 16 ++- src/leap/bitmask/core/dispatcher.py | 57 +++++---- src/leap/bitmask/core/dummy.py | 56 ++++++--- src/leap/bitmask/core/launcher.py | 1 + src/leap/bitmask/core/service.py | 1 + src/leap/bitmask/core/web/_auth.py | 3 - src/leap/bitmask/core/web/service.py | 8 +- tests/unit/core/test_web_api.py | 142 ++++++++++++++++------ 11 files changed, 428 insertions(+), 92 deletions(-) diff --git a/docs/core/core_api_contract b/docs/core/core_api_contract index b70fb8f..ba4a963 100755 --- a/docs/core/core_api_contract +++ b/docs/core/core_api_contract @@ -21,6 +21,8 @@ api for Bitmask Core. The values are meant to be type annotations. """ +import pkg_resources +pkg_resources.get_distribution('leap.bitmask') if __name__ == "__main__": from leap.bitmask.core.service import BitmaskBackend diff --git a/docs/core/index.rst b/docs/core/index.rst index c52dcc1..9740744 100644 --- a/docs/core/index.rst +++ b/docs/core/index.rst @@ -4,10 +4,230 @@ .. _bitmask_core: +============ Bitmask Core -================================ -blah blah +============ -API documentation --------------------------------- +The bitmask core daemon can be launched like this:: + bitmaskd + +The command-line program, ``bitmaskctl``, and the GUI, will launch the +daemon when needed. + +Starting the API server +======================= + +If configured to do so, the bitmask core will expose all of the commands +throught a REST API. In bitmaskd.cfg:: + + [services] + web = True + + +Resources +========= + +Following is a list of currently available resources and a brief description of +each one. For details click on the resource name. + ++-----------------------------------+---------------------------------+ +| Resource | Description | ++===================================+=================================+ +| ``POST`` :ref:`cmd_core_version` | Get Bitmask Core Version Info | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_core_stats` | Get Stats about Bitmask Usage | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_core_status` | Get Bitmask Status | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_core_stop` | Stop Bitmask Core | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_prov_list` | List all providers | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_prov_create` | Create a new provider | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_prov_read` | Get info about a provider | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_prov_del` | Delete a given provider | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_user_list` | List all users | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_user_active` | Get active user | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_user_create` | Create a new user | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_user_update` | Update an user | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_user_auth` | Authenticate an user | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_user_logout` | End session for an user | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_keys_list` | Get all known keys for an user | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_keys_insert` | Insert a new key | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_keys_del` | Delete a given key | ++-----------------------------------+---------------------------------+ +| ``POST`` :ref:`cmd_keys_export` | Export keys | ++-----------------------------------+---------------------------------+ + +.. _cmd_core_version: + +/core/version +------------- +**POST /core/version** + + Get Bitmask Core Version Info + +.. _cmd_core_stats: + +/core/stats +----------- +**POST /core/stats** + + Get Stats about Bitmask Usage + +.. _cmd_core_status: + +/core/status +------------ +**POST /core/status** + + Get Bitmask status + +.. _cmd_core_stop: + +/core/stop +---------- +**POST /core/stop** + + Stop Bitmask core (daemon shutdown). + +.. _cmd_prov_list: + +/bonafide/provider/list +----------------------- +**POST /bonafide/provider/list** + + List all known providers. + +.. _cmd_prov_create: + +/bonafide/provider/create +-------------------------- +**POST /bonafide/provider** + + Create a new provider. + +.. _cmd_prov_read: + +/bonafide/provider/read +----------------------- +**POST /bonafide/provider/read** + + Get info bout a given provider. + +.. _cmd_prov_del: + +/bonafide/provider/delete +------------------------- +**POST /bonafide/provider/delete** + + Delete a given provider. + + +.. _cmd_user_list: + +/bonafide/user/list +------------------- +**POST /bonafide/user/list** + + List all the users known to the local backend. + + **Form parameters**: + * ``foo`` *(required)* - foo bar. + * ``bar`` *(optional)* - foo bar. + + **Status codes**: + * ``200`` - no error + +.. _cmd_user_active: + +/bonafide/user/active +--------------------- +**POST /bonafide/user/active** + + Get the active user. + +.. _cmd_user_create: + +/bonafide/user/create +--------------------- +**POST /bonafide/user/create** + + Create a new user. + + **Form parameters**: + * ``foo`` *(required)* - foo bar. + +.. _cmd_user_update: + +/bonafide/user/update +--------------------- +**POST /bonafide/user/update** + + Update a given user. + +.. _cmd_user_auth: + +/bonafide/user/authenticate +--------------------------- +**POST /bonafide/user/authenticate** + + Authenticate an user. + +.. _cmd_user_logout: + +/bonafide/user/logout +--------------------- +**POST /bonafide/user/logout** + + Logs out an user, and destroys its local session. + +.. _cmd_keys_list: + +/keys/list +------------------- +**POST /keys/list** + + Get all keys for an user. + +.. _cmd_keys_insert: + +/keys/insert/ +------------------- +**POST /keys/insert** + + Insert a new key for an user. + +.. _cmd_keys_del: + +/keys/delete/ +------------------- +**POST /keys/delete** + + Delete a key for an user. + +.. _cmd_keys_export: + +/keys/export/ +------------------- +**POST /keys/export** + + Export keys for an user. + + +API Authentication +================== + +(TBD) Most of the resources in the API are protected by an authentication token. diff --git a/src/leap/bitmask/__init__.py b/src/leap/bitmask/__init__.py index 20719d4..6fd6174 100644 --- a/src/leap/bitmask/__init__.py +++ b/src/leap/bitmask/__init__.py @@ -2,9 +2,6 @@ import sys import pkg_resources from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions - if not getattr(sys, 'frozen', False): # FIXME: HACK for https://github.com/pypa/pip/issues/3 # Without this 'fix', there are resolution conflicts when pip installs at @@ -12,3 +9,6 @@ if not getattr(sys, 'frozen', False): # namespace from pypi. For instance: # 'pip install -e .' and 'pip install leap.common' pkg_resources.get_distribution('leap.bitmask') + +__version__ = get_versions()['version'] +del get_versions diff --git a/src/leap/bitmask/cli/bitmask_cli.py b/src/leap/bitmask/cli/bitmask_cli.py index 893b7d1..dfd1fbc 100755 --- a/src/leap/bitmask/cli/bitmask_cli.py +++ b/src/leap/bitmask/cli/bitmask_cli.py @@ -60,7 +60,6 @@ GENERAL COMMANDS: ''' epilog = ("Use 'bitmaskctl help' to learn more " "about each command.") - commands = ['stop', 'stats'] def user(self, raw_args): user = User() @@ -99,7 +98,7 @@ GENERAL COMMANDS: return defer.succeed(None) def version(self, raw_args): - self.data = ['version'] + self.data = ['core', 'version'] return self._send(printer=self._print_version) def _print_version(self, version): @@ -107,7 +106,7 @@ GENERAL COMMANDS: print(Fore.GREEN + 'bitmask_core: ' + Fore.RESET + corever) def status(self, raw_args): - self.data = ['status'] + self.data = ['core', 'status'] return self._send(printer=self._print_status) def _print_status(self, status): @@ -119,11 +118,19 @@ GENERAL COMMANDS: print(key.ljust(10) + ': ' + color + value + Fore.RESET) + def stop(self, raw_args): + self.data = ['core', 'stop'] + return self._send(printer=command.default_dict_printer) + + def stats(self, raw_args): + self.data = ['core', 'stats'] + return self._send(printer=command.default_dict_printer) + @defer.inlineCallbacks def execute(): cli = BitmaskCLI() - cli.data = ['version'] + cli.data = ['core', 'version'] args = ['--verbose'] if '--verbose' in sys.argv else None yield cli._send( timeout=0.1, printer=_null_printer, @@ -152,5 +159,6 @@ def main(): signal.signal(signal.SIGINT, signal_handler) reactor.run() + if __name__ == "__main__": main() diff --git a/src/leap/bitmask/core/dispatcher.py b/src/leap/bitmask/core/dispatcher.py index 5900390..b068683 100644 --- a/src/leap/bitmask/core/dispatcher.py +++ b/src/leap/bitmask/core/dispatcher.py @@ -38,6 +38,10 @@ from .api import APICommand, register_method logger = Logger() +class DispatchError(Exception): + pass + + class SubCommand(object): __metaclass__ = APICommand @@ -112,6 +116,8 @@ class UserCmd(SubCommand): @register_method("{'signup': 'ok', 'user': str}") def do_CREATE(self, bonafide, *parts): + if len(parts) < 5: + raise DispatchError('Not enough parameters passed') # params are: [user, create, full_id, password, invite, autoconf] user, password, invite = parts[2], parts[3], parts[4] @@ -219,7 +225,6 @@ class WebUICmd(SubCommand): @register_method('dict') def do_STATUS(self, webui, *parts, **kw): - print 'webui', webui d = webui.do_status() return d @@ -310,7 +315,6 @@ class EventsCmd(SubCommand): self.waiting.append(d) return d - @register_method("") def _callback(self, event, *content): payload = (str(event), content) if not self.waiting: @@ -322,15 +326,35 @@ class EventsCmd(SubCommand): d.callback(payload) +class CoreCmd(SubCommand): + + label = 'core' + + @register_method("{'mem_usage': str}") + def do_STATS(self, core, *parts): + return core.do_stats() + + @register_method("{version_core': '0.0.0'}") + def do_VERSION(self, core, *parts): + return core.do_version() + + @register_method("{'mail': 'running'}") + def do_STATUS(self, core, *parts): + return core.do_status() + + @register_method("{'stop': 'ok'}") + def do_STOP(self, core, *parts): + return core.do_stop() + + class CommandDispatcher(object): __metaclass__ = APICommand - label = 'core' - def __init__(self, core): self.core = core + self.subcommand_core = CoreCmd() self.subcommand_bonafide = BonafideCmd() self.subcommand_eip = EIPCmd() self.subcommand_mail = MailCmd() @@ -338,26 +362,10 @@ class CommandDispatcher(object): self.subcommand_events = EventsCmd() self.subcommand_webui = WebUICmd() - # XXX -------------------------------------------- - # TODO move general services to another subclass - - @register_method("{'mem_usage': str}") - def do_STATS(self, *parts): - return _format_result(self.core.do_stats()) - - @register_method("{version_core': '0.0.0'}") - def do_VERSION(self, *parts): - return _format_result(self.core.do_version()) - - @register_method("{'mail': 'running'}") - def do_STATUS(self, *parts): - return _format_result(self.core.do_status()) - - @register_method("{'stop': 'ok'}") - def do_STOP(self, *parts): - return _format_result(self.core.do_stop()) - - # ----------------------------------------------- + def do_CORE(self, *parts): + d = self.subcommand_core.dispatch(self.core, *parts) + d.addCallbacks(_format_result, _format_error) + return d def do_BONAFIDE(self, *parts): bonafide = self._get_service('bonafide') @@ -446,7 +454,6 @@ class CommandDispatcher(object): def dispatch(self, msg): cmd = msg[0] - _method = getattr(self, 'do_' + cmd.upper(), None) if not _method: diff --git a/src/leap/bitmask/core/dummy.py b/src/leap/bitmask/core/dummy.py index 455756c..2037b81 100644 --- a/src/leap/bitmask/core/dummy.py +++ b/src/leap/bitmask/core/dummy.py @@ -22,6 +22,34 @@ import json from leap.bitmask.hooks import HookableService +class CannedData: + + class backend: + status = { + 'soledad': 'running', + 'keymanager': 'running', + 'mail': 'running', + 'eip': 'stopped', + 'backend': 'dummy'} + version = {'version_core': '0.0.1'} + stop = {'stop': 'ok'} + stats = {'mem_usage': '01 KB'} + + class bonafide: + auth = { + u'srp_token': u'deadbeef123456789012345678901234567890123', + u'uuid': u'01234567890abcde01234567890abcde'} + signup = { + 'signup': 'ok', + 'user': 'dummyuser@provider.example.org'} + list_users = { + 'userid': 'testuser', + 'authenticated': False} + logout = { + 'logout': 'ok'} + get_active_user = 'dummyuser@provider.example.org' + + class BackendCommands(object): """ @@ -30,23 +58,19 @@ class BackendCommands(object): def __init__(self, core): self.core = core + self.canned = CannedData def do_status(self): - return json.dumps( - {'soledad': 'running', - 'keymanager': 'running', - 'mail': 'running', - 'eip': 'stopped', - 'backend': 'dummy'}) + return json.dumps(self.canned.backend.stats) def do_version(self): - return {'version_core': '0.0.1'} + return self.canned.backend.version def do_stats(self): - return {'mem_usage': '01 KB'} + return self.canned.backend.stats def do_stop(self): - return {'stop': 'ok'} + return self.canned.backend.stop class mail_services(object): @@ -64,17 +88,19 @@ class mail_services(object): class BonafideService(HookableService): def __init__(self, basedir): - pass + self.canned = CannedData def do_authenticate(self, user, password): - return {u'srp_token': u'deadbeef123456789012345678901234567890123', - u'uuid': u'01234567890abcde01234567890abcde'} + return self.canned.bonafide.auth def do_signup(self, user, password): - return {'signup': 'ok', 'user': 'dummyuser@provider.example.org'} + return self.canned.bonafide.signup + + def do_list_users(self): + return self.canned.bonafide.list_users def do_logout(self, user): - return {'logout': 'ok'} + return self.canned.bonafide.logout def do_get_active_user(self): - return 'dummyuser@provider.example.org' + return self.canned.bonafide.get_active_user diff --git a/src/leap/bitmask/core/launcher.py b/src/leap/bitmask/core/launcher.py index a1c8690..14d8e60 100644 --- a/src/leap/bitmask/core/launcher.py +++ b/src/leap/bitmask/core/launcher.py @@ -45,6 +45,7 @@ def here(module=None): def run_bitmaskd(): + # TODO --- configure where to put the logs... (get --logfile, --logdir # from bitmaskctl for (index, arg) in enumerate(sys.argv): diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py index 9682c18..c3e97f7 100644 --- a/src/leap/bitmask/core/service.py +++ b/src/leap/bitmask/core/service.py @@ -266,6 +266,7 @@ class BackendCommands(object): return {'version_core': __version__} def do_stats(self): + print "DO STATS" logger.debug('BitmaskCore Service STATS') mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss return {'mem_usage': '%s MB' % (mem / 1024)} diff --git a/src/leap/bitmask/core/web/_auth.py b/src/leap/bitmask/core/web/_auth.py index 6a5e362..3eb4fa1 100644 --- a/src/leap/bitmask/core/web/_auth.py +++ b/src/leap/bitmask/core/web/_auth.py @@ -18,9 +18,6 @@ class WhitelistHTTPAuthSessionWrapper(HTTPAuthSessionWrapper): It doesn't apply the enforcement to routes included in a whitelist. """ - # TODO extend this to inspect the data -- so that we pass a tuple - # with the action - whitelist = (None,) def __init__(self, *args, **kw): diff --git a/src/leap/bitmask/core/web/service.py b/src/leap/bitmask/core/web/service.py index 2437d2d..77e1c72 100644 --- a/src/leap/bitmask/core/web/service.py +++ b/src/leap/bitmask/core/web/service.py @@ -59,7 +59,13 @@ class HTTPDispatcherService(service.Service): """ API_WHITELIST = ( - '/API/bonafide/user', + '/API/core/version', + '/API/core/stats', + '/API/bonafide/user/create', + '/API/bonafide/user/authenticate', + '/API/bonafide/provider/list', + '/API/bonafide/provider/create', + '/API/bonafide/provider/read', ) def __init__(self, core, port=7070, debug=False, onion=False): diff --git a/tests/unit/core/test_web_api.py b/tests/unit/core/test_web_api.py index ae10ec4..02708f6 100644 --- a/tests/unit/core/test_web_api.py +++ b/tests/unit/core/test_web_api.py @@ -1,16 +1,22 @@ +import json import base64 from twisted.application import service from twisted.cred import portal +from twisted.internet import defer, reactor +from twisted.python.compat import networkString from twisted.trial import unittest -from twisted.web.test.test_web import DummyRequest +from twisted.web import client from twisted.web import resource from twisted.web.server import Site +from twisted.web.test.test_web import DummyRequest from leap.bitmask.core import dispatcher from leap.bitmask.core import web from leap.bitmask.core.dummy import mail_services from leap.bitmask.core.dummy import BonafideService +from leap.bitmask.core.dummy import BackendCommands +from leap.bitmask.core.dummy import CannedData def b64encode(s): @@ -102,47 +108,106 @@ class WhitelistedResourceTests(SimpleAPIMixin, unittest.TestCase): pass -class RESTApiMixin: - def setUp(self): - self.request = self.makeRequest() +class AuthTestResource(resource.Resource): - dispatcher = dummyDispatcherFactory() - api = web.api.Api(dispatcher) - self.site = Site(api) + isLeaf = True - def makeRequest(self, method=b'GET', clientAddress=None): - """ - Create a request object to be passed to - TokenCredentialFactory.decode along with a response value. - Override this in a subclass. - """ - raise NotImplementedError("%r did not implement makeRequest" % ( - self.__class__,)) + def render_GET(self, request): + return "dummyGET" + + def render_POST(self, request): + return "dummyPOST" -class RESTApiTests(RESTApiMixin, unittest.TestCase): +class RESTApiTests(unittest.TestCase): """ Tests that involve checking the routing between the REST api and the command dispatcher. - """ - def test_simple_api_request(self): - # FIXME -- check the requests to the API - assert 1 == 1 + This is just really testing the canned responses in the Dummy backend. + To make sure that those responses match the live data, e2e tests should be + run. + """ - def makeRequest(self, method='GET', clientAddress=None): - pass + def setUp(self): + dispatcher = dummyDispatcherFactory() + api = web.api.Api(dispatcher) + plainSite = Site(api) + self.plainPort = reactor.listenTCP(0, plainSite, interface="127.0.0.1") + self.plainPortno = self.plainPort.getHost().port + self.canned = CannedData + + def tearDown(self): + return self.plainPort.stopListening() + + # core commands + + @defer.inlineCallbacks + def test_core_version(self): + call = yield self.makeAPICall('core/version') + self.assertCall(call, self.canned.backend.version) + + @defer.inlineCallbacks + def test_core_stop(self): + call = yield self.makeAPICall('core/stop') + self.assertCall(call, self.canned.backend.stop) + + # bonafide commands + + @defer.inlineCallbacks + def test_bonafide_user_list(self): + call = yield self.makeAPICall('bonafide/user/list') + self.assertCall(call, self.canned.bonafide.list_users) + + @defer.inlineCallbacks + def test_bonafide_user_create(self): + call = yield self.makeAPICall('bonafide/user/create') + self.assertCall(call, self.canned.bonafide.auth) + + @defer.inlineCallbacks + def test_bonafide_user_update(self): + call = yield self.makeAPICall('bonafide/user/update') + self.assertCall(call, self.canned.bonafide.update) + + @defer.inlineCallbacks + def test_bonafide_user_authenticate(self): + call = yield self.makeAPICall('bonafide/user/authenticate') + self.assertCall(call, self.canned.bonafide.auth) + + @defer.inlineCallbacks + def test_bonafide_user_active(self): + call = yield self.makeAPICall('bonafide/user/active') + self.assertCall(call, self.canned.bonafide.get_active_user) + + @defer.inlineCallbacks + def test_bonafide_user_logout(self): + call = yield self.makeAPICall('bonafide/user/logout') + self.assertCall(call, self.canned.bonafide.logout) + + def makeAPICall(self, path, method="POST"): + uri = networkString("http://127.0.0.1:%d/%s" % ( + self.plainPortno, path)) + return client.getPage(uri, method=method, timeout=1) + + def assertCall(self, returned, expected): + data = json.loads(returned) + error = data['error'] + assert error is None + result = data['result'] + assert result == expected class DummyCore(service.MultiService): + """ A minimal core that uses the dummy backend modules. """ def __init__(self): service.MultiService.__init__(self) - mail = mail_services.StandardMailService - self.init('mail', mail) + + bf = BonafideService + self.init('bonafide', bf, '/tmp/') km = mail_services.KeymanagerService self.init('keymanager', km) @@ -150,14 +215,28 @@ class DummyCore(service.MultiService): sol = mail_services.SoledadService self.init('soledad', sol) - bf = BonafideService - self.init('bonafide', bf, '/tmp/') + mail = mail_services.StandardMailService + self.init('mail', mail) + + self.core_cmds = BackendCommands(self) def init(self, label, service, *args, **kw): s = service(*args, **kw) s.setName(label) s.setServiceParent(self) + def do_stats(self): + return self.core_cmds.do_stats() + + def do_version(self): + return self.core_cmds.do_version() + + def do_status(self): + return self.core_cmds.do_status() + + def do_stop(self): + return self.core_cmds.do_stop() + def dummyDispatcherFactory(): """ @@ -165,14 +244,3 @@ def dummyDispatcherFactory(): """ dummy_core = DummyCore() return dispatcher.CommandDispatcher(dummy_core) - - -class AuthTestResource(resource.Resource): - - isLeaf = True - - def render_GET(self, request): - return "dummyGET" - - def render_POST(self, request): - return "dummyPOST" -- cgit v1.2.3