diff options
| -rwxr-xr-x | docs/core/core_api_contract | 2 | ||||
| -rw-r--r-- | docs/core/index.rst | 228 | ||||
| -rw-r--r-- | src/leap/bitmask/__init__.py | 6 | ||||
| -rwxr-xr-x | src/leap/bitmask/cli/bitmask_cli.py | 16 | ||||
| -rw-r--r-- | src/leap/bitmask/core/dispatcher.py | 57 | ||||
| -rw-r--r-- | src/leap/bitmask/core/dummy.py | 56 | ||||
| -rw-r--r-- | src/leap/bitmask/core/launcher.py | 1 | ||||
| -rw-r--r-- | src/leap/bitmask/core/service.py | 1 | ||||
| -rw-r--r-- | src/leap/bitmask/core/web/_auth.py | 3 | ||||
| -rw-r--r-- | src/leap/bitmask/core/web/service.py | 8 | ||||
| -rw-r--r-- | 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 b70fb8fa..ba4a963f 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 c52dcc17..9740744d 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 20719d47..6fd6174d 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 893b7d11..dfd1fbcd 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 <command> 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 59003906..b068683d 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 455756c4..2037b81f 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 a1c8690f..14d8e607 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 9682c18c..c3e97f72 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 6a5e3621..3eb4fa13 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 2437d2d6..77e1c729 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 ae10ec41..02708f61 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" | 
