From 59cd23bd3e23bf2b439ad26271733a1b5c8edf68 Mon Sep 17 00:00:00 2001
From: Ruben Pollan <meskio@sindominio.net>
Date: Thu, 9 Feb 2017 17:24:25 +0100
Subject: [feat] eliminate the active user from bonafide

Active user is now only a concept of the cli. For it we add a
~/.config/leap/bitmaskctl.cfg file.

- Resolves: #8769
---
 src/leap/bitmask/bonafide/service.py  | 25 +----------
 src/leap/bitmask/cli/bitmask_cli.py   | 16 ++++---
 src/leap/bitmask/cli/command.py       |  4 +-
 src/leap/bitmask/cli/keys.py          | 24 ++++++++--
 src/leap/bitmask/cli/mail.py          |  9 +++-
 src/leap/bitmask/cli/user.py          | 33 +++++++++-----
 src/leap/bitmask/config.py            | 84 +++++++++++++++++++++++++++++++++++
 src/leap/bitmask/core/configurable.py | 74 +++++-------------------------
 src/leap/bitmask/core/dispatcher.py   | 45 +++++--------------
 9 files changed, 171 insertions(+), 143 deletions(-)
 create mode 100644 src/leap/bitmask/config.py

(limited to 'src')

diff --git a/src/leap/bitmask/bonafide/service.py b/src/leap/bitmask/bonafide/service.py
index d48a54a7..37d1e214 100644
--- a/src/leap/bitmask/bonafide/service.py
+++ b/src/leap/bitmask/bonafide/service.py
@@ -42,10 +42,6 @@ class BonafideService(HookableService):
         self._bonafide = BonafideProtocol()
         self.service_hooks = defaultdict(list)
 
-        # XXX this is a quick hack to get a ref
-        # to the latest authenticated user.
-        self._active_user = None
-
     def startService(self):
         logger.debug('starting Bonafide Service')
         super(BonafideService, self).startService()
@@ -68,8 +64,6 @@ class BonafideService(HookableService):
             data = dict(username=username, token=token, uuid=uuid,
                         password=password)
             self.trigger_hook('on_bonafide_auth', **data)
-
-            self._active_user = username
             return result
 
         # XXX I still have doubts from where it's best to trigger this.
@@ -93,18 +87,10 @@ class BonafideService(HookableService):
         return d
 
     def do_logout(self, username):
-        if not username:
-            username = self._active_user
-
-        def reset_active(passthrough):
-            self._active_user = None
-            return passthrough
-
         data = dict(username=username)
         self.trigger_hook('on_bonafide_logout', **data)
 
         d = self._bonafide.do_logout(username)
-        d.addCallback(reset_active)
         d.addCallback(lambda response: {'logout': 'ok'})
         return d
 
@@ -134,18 +120,11 @@ class BonafideService(HookableService):
     def do_provider_list(self, seeded=False):
         return self._bonafide.do_provider_list(seeded)
 
-    def do_get_smtp_cert(self, username=None):
-        if not username:
-            username = self._active_user
+    def do_get_smtp_cert(self, username):
         if not username:
             return defer.fail(
-                RuntimeError('No active user, cannot get SMTP cert.'))
+                RuntimeError('No username, cannot get SMTP cert.'))
 
         d = self._bonafide.do_get_smtp_cert(username)
         d.addCallback(lambda response: (username, response))
         return d
-
-    def do_get_active_user(self):
-        user = self._active_user or '<none>'
-        info = {'user': user}
-        return defer.succeed(info)
diff --git a/src/leap/bitmask/cli/bitmask_cli.py b/src/leap/bitmask/cli/bitmask_cli.py
index dfd1fbcd..782a52e5 100755
--- a/src/leap/bitmask/cli/bitmask_cli.py
+++ b/src/leap/bitmask/cli/bitmask_cli.py
@@ -25,6 +25,7 @@ import signal
 from colorama import Fore
 from twisted.internet import reactor, defer
 
+from leap.bitmask.config import Configuration
 from leap.bitmask.cli.eip import Eip
 from leap.bitmask.cli.keys import Keys
 from leap.bitmask.cli.mail import Mail
@@ -62,27 +63,27 @@ GENERAL COMMANDS:
               "about each command.")
 
     def user(self, raw_args):
-        user = User()
+        user = User(self.cfg)
         return user.execute(raw_args)
 
     def mail(self, raw_args):
-        mail = Mail()
+        mail = Mail(self.cfg)
         return mail.execute(raw_args)
 
     def eip(self, raw_args):
-        eip = Eip()
+        eip = Eip(self.cfg)
         return eip.execute(raw_args)
 
     def keys(self, raw_args):
-        keys = Keys()
+        keys = Keys(self.cfg)
         return keys.execute(raw_args)
 
     def ui(self, raw_args):
-        webui = WebUI()
+        webui = WebUI(self.cfg)
         return webui.execute(raw_args)
 
     def logs(self, raw_args):
-        logs = Logs()
+        logs = Logs(self.cfg)
         return logs.execute(raw_args)
 
     # Single commands
@@ -129,7 +130,8 @@ GENERAL COMMANDS:
 
 @defer.inlineCallbacks
 def execute():
-    cli = BitmaskCLI()
+    cfg = Configuration("bitmaskctl.cfg")
+    cli = BitmaskCLI(cfg)
     cli.data = ['core', 'version']
     args = ['--verbose'] if '--verbose' in sys.argv else None
     yield cli._send(
diff --git a/src/leap/bitmask/cli/command.py b/src/leap/bitmask/cli/command.py
index 16f483a3..95b0fe8d 100644
--- a/src/leap/bitmask/cli/command.py
+++ b/src/leap/bitmask/cli/command.py
@@ -57,7 +57,9 @@ class Command(object):
               "about each command.")
     commands = []
 
-    def __init__(self):
+    def __init__(self, cfg):
+        self.cfg = cfg
+
         color_init()
         zf = ZmqFactory()
         e = ZmqEndpoint(ZmqEndpointType.connect, ENDPOINT)
diff --git a/src/leap/bitmask/cli/keys.py b/src/leap/bitmask/cli/keys.py
index 65747cb8..b11d2801 100644
--- a/src/leap/bitmask/cli/keys.py
+++ b/src/leap/bitmask/cli/keys.py
@@ -50,7 +50,11 @@ SUBCOMMANDS:
                             help='Use private keys (by default uses public)')
         subargs = parser.parse_args(raw_args)
 
-        self.data += ['list', subargs.uid]
+        userid = subargs.userid
+        if not userid:
+            userid = self.cfg.get('bonafide', 'active', default='')
+
+        self.data += ['list', userid]
         if subargs.private:
             self.data += ['private']
         else:
@@ -69,7 +73,11 @@ SUBCOMMANDS:
         parser.add_argument('address', nargs=1,
                             help='email address of the key')
         subargs = parser.parse_args(raw_args)
-        self.data += ['export', subargs.uid, subargs.address[0]]
+
+        userid = subargs.userid
+        if not userid:
+            userid = self.cfg.get('bonafide', 'active', default='')
+        self.data += ['export', userid, subargs.address[0]]
 
         return self._send(self._print_key)
 
@@ -88,9 +96,13 @@ SUBCOMMANDS:
                             help='email address of the key')
         subargs = parser.parse_args(raw_args)
 
+        userid = subargs.userid
+        if not userid:
+            userid = self.cfg.get('bonafide', 'active')
+
         with open(subargs.file[0], 'r') as keyfile:
             rawkey = keyfile.read()
-        self.data += ['insert', subargs.uid, subargs.address[0],
+        self.data += ['insert', userid, subargs.address[0],
                       subargs.validation, rawkey]
 
         return self._send(self._print_key)
@@ -106,8 +118,12 @@ SUBCOMMANDS:
         parser.add_argument('address', nargs=1,
                             help='email address of the key')
         subargs = parser.parse_args(raw_args)
-        self.data += ['delete', subargs.uid, subargs.address[0]]
 
+        userid = subargs.userid
+        if not userid:
+            userid = self.cfg.get('bonafide', 'active')
+
+        self.data += ['delete', userid, subargs.address[0]]
         return self._send()
 
     def _print_key_list(self, keys):
diff --git a/src/leap/bitmask/cli/mail.py b/src/leap/bitmask/cli/mail.py
index 7fd574b9..025804eb 100644
--- a/src/leap/bitmask/cli/mail.py
+++ b/src/leap/bitmask/cli/mail.py
@@ -51,8 +51,15 @@ SUBCOMMANDS:
         subargs = parser.parse_args(raw_args)
 
         self.data.append('status')
+
+        uid = None
         if subargs.uid:
-            self.data.append(subargs.uid)
+            uid = subargs.uid
+        else:
+            uid = self.cfg.get('bonafide', 'active', default=None)
+        if uid:
+            self.data.append(uid)
+
         return self._send(self._print_status)
 
     def _print_status(self, status, depth=0):
diff --git a/src/leap/bitmask/cli/user.py b/src/leap/bitmask/cli/user.py
index b802d86b..a5cad1b0 100644
--- a/src/leap/bitmask/cli/user.py
+++ b/src/leap/bitmask/cli/user.py
@@ -23,6 +23,7 @@ import sys
 from copy import copy
 
 from colorama import Fore
+from twisted.internet import defer
 
 from leap.bitmask.cli import command
 
@@ -44,10 +45,8 @@ SUBCOMMANDS:
 
 '''.format(name=command.appname)
 
-    commands = ['active']
-
-    def __init__(self):
-        super(User, self).__init__()
+    def __init__(self, cfg):
+        super(User, self).__init__(cfg)
         self.data.append('user')
 
     def create(self, raw_args):
@@ -75,7 +74,7 @@ SUBCOMMANDS:
                 args.pop(index + 1)
                 args.pop(index)
 
-        username = self.username(args)
+        username = self._username(args, needed=True)
         if not passwd:
             passwd = self._getpass_twice()
         self.data += ['create', username, passwd,
@@ -89,15 +88,20 @@ SUBCOMMANDS:
                 passwd = raw_args.pop(index + 1)
                 raw_args.pop(index)
 
-        username = self.username(raw_args)
+        username = self._username(raw_args, needed=True)
         if not passwd:
             passwd = getpass.getpass()
         self.data += ['authenticate', username, passwd, 'true']
+        self.cfg.set('bonafide', 'active', username)
         return self._send(printer=command.default_dict_printer)
 
     def logout(self, raw_args):
-        username = self.username(raw_args)
+        username = self._username(raw_args)
         self.data += ['logout', username]
+
+        active = self.cfg.get('bonafide', 'active', default=None)
+        if active == username:
+            self.cfg.set('bonafide', 'active', "")
         return self._send(printer=command.default_dict_printer)
 
     def list(self, raw_args):
@@ -105,13 +109,19 @@ SUBCOMMANDS:
         return self._send(printer=self._print_user_list)
 
     def update(self, raw_args):
-        username = self.username(raw_args)
+        username = self._username(raw_args)
         current_passwd = getpass.getpass('Current password: ')
         new_passwd = self._getpass_twice('New password: ')
         self.data += ['update', username, current_passwd, new_passwd]
         return self._send(printer=command.default_dict_printer)
 
-    def username(self, raw_args):
+    def active(self, raw_args):
+        username = self.cfg.get('bonafide', 'active', default='<none>')
+        print(Fore.RESET + 'active'.ljust(10) + Fore.GREEN + username +
+              Fore.RESET)
+        return defer.succeed(None)
+
+    def _username(self, raw_args, needed=False):
         args = tuple([command.appname] + sys.argv[1:3])
         parser = argparse.ArgumentParser(
             description='Bitmask user',
@@ -121,7 +131,10 @@ SUBCOMMANDS:
 
         username = subargs.username
         if not username:
-            self._error("Missing username ID but needed for this command")
+            if needed:
+                self._error("Missing username ID but needed for this command")
+            else:
+                return self.cfg.get('bonafide', 'active')
         if '@' not in username:
             self._error("Username ID must be in the form <user@example.org>")
 
diff --git a/src/leap/bitmask/config.py b/src/leap/bitmask/config.py
new file mode 100644
index 00000000..a09a2bc5
--- /dev/null
+++ b/src/leap/bitmask/config.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+# config.py
+# Copyright (C) 2017 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+Configuration parser/writter
+"""
+import ConfigParser
+import os
+
+from leap.common import files
+from leap.common.config import get_path_prefix
+
+
+DEFAULT_BASEDIR = os.path.join(get_path_prefix(), 'leap')
+
+
+class MissingConfigEntry(Exception):
+    """
+    A required config entry was not found.
+    """
+
+
+class Configuration(object):
+
+    def __init__(self, config_file, basedir=DEFAULT_BASEDIR,
+                 default_config=""):
+        path = os.path.abspath(os.path.expanduser(basedir))
+        if not os.path.isdir(path):
+            files.mkdir_p(path)
+        self.config_path = os.path.join(path, config_file)
+        self.default_config = default_config
+
+        self.read()
+
+    def get(self, section, option, default=None, boolean=False):
+        try:
+            if boolean:
+                return self.config.getboolean(section, option)
+
+            item = self.config.get(section, option)
+            return item
+
+        except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
+            if default is None:
+                raise MissingConfigEntry("%s is missing the [%s]%s entry"
+                                         % self.config_path, section, option)
+            return default
+
+    def set(self, section, option, value):
+        if not self.config.has_section(section):
+            self.config.add_section(section)
+        self.config.set(section, option, value)
+        self.save()
+        self.read()
+        assert self.config.get(section, option) == value
+
+    def read(self):
+        self.config = ConfigParser.SafeConfigParser()
+        if not os.path.isfile(self.config_path):
+            self._create_default_config()
+
+        with open(self.config_path, "rb") as f:
+            self.config.readfp(f)
+
+    def save(self):
+        with open(self.config_path, 'wb') as f:
+            self.config.write(f)
+
+    def _create_default_config(self):
+        with open(self.config_path, 'w') as outf:
+            outf.write(self.default_config)
diff --git a/src/leap/bitmask/core/configurable.py b/src/leap/bitmask/core/configurable.py
index f305cc3c..51a100d6 100644
--- a/src/leap/bitmask/core/configurable.py
+++ b/src/leap/bitmask/core/configurable.py
@@ -17,22 +17,13 @@
 """
 Configurable Backend for Bitmask Service.
 """
-import ConfigParser
-import os
-
 from twisted.application import service
 
-from leap.common import files
-from leap.common.config import get_path_prefix
-
-
-DEFAULT_BASEDIR = os.path.join(get_path_prefix(), 'leap')
-
-
-class MissingConfigEntry(Exception):
-    """
-    A required config entry was not found.
-    """
+from leap.bitmask.config import (
+    Configuration,
+    DEFAULT_BASEDIR,
+    MissingConfigEntry
+)
 
 
 class ConfigurableService(service.MultiService):
@@ -42,59 +33,14 @@ class ConfigurableService(service.MultiService):
 
     def __init__(self, basedir=DEFAULT_BASEDIR):
         service.MultiService.__init__(self)
-
-        path = os.path.abspath(os.path.expanduser(basedir))
-        if not os.path.isdir(path):
-            files.mkdir_p(path)
-        self.basedir = path
-
-        # creates self.config
-        self.read_config()
+        self.cfg = Configuration(self.config_file, basedir, DEFAULT_CONFIG)
+        self.basedir = basedir
 
     def get_config(self, section, option, default=None, boolean=False):
-        try:
-            if boolean:
-                return self.config.getboolean(section, option)
-
-            item = self.config.get(section, option)
-            return item
-
-        except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
-            if default is None:
-                fn = self._get_config_path()
-                raise MissingConfigEntry("%s is missing the [%s]%s entry"
-                                         % fn, section, option)
-            return default
+        return self.cfg.get(section, option, default=default, boolean=boolean)
 
     def set_config(self, section, option, value):
-        if not self.config.has_section(section):
-            self.config.add_section(section)
-        self.config.set(section, option, value)
-        self.save_config()
-        self.read_config()
-        assert self.config.get(section, option) == value
-
-    def read_config(self):
-        self.config = ConfigParser.SafeConfigParser()
-        bitmaskd_cfg = self._get_config_path()
-
-        if not os.path.isfile(bitmaskd_cfg):
-            self._create_default_config(bitmaskd_cfg)
-
-        with open(bitmaskd_cfg, "rb") as f:
-            self.config.readfp(f)
-
-    def save_config(self):
-        bitmaskd_cfg = self._get_config_path()
-        with open(bitmaskd_cfg, 'wb') as f:
-            self.config.write(f)
-
-    def _create_default_config(self, path):
-        with open(path, 'w') as outf:
-            outf.write(DEFAULT_CONFIG)
-
-    def _get_config_path(self):
-        return os.path.join(self.basedir, self.config_file)
+        return self.cfg.set(section, option, value)
 
 
 DEFAULT_CONFIG = """
@@ -106,3 +52,5 @@ web = True
 onion = False
 websockets = False
 """
+
+__all__ = ["ConfigurableService", DEFAULT_BASEDIR, MissingConfigEntry]
diff --git a/src/leap/bitmask/core/dispatcher.py b/src/leap/bitmask/core/dispatcher.py
index 2777d9a9..1e95ce28 100644
--- a/src/leap/bitmask/core/dispatcher.py
+++ b/src/leap/bitmask/core/dispatcher.py
@@ -174,10 +174,6 @@ class UserCmd(SubCommand):
         return bonafide.do_change_password(
             user, current_password, new_password)
 
-    @register_method('str')
-    def do_ACTIVE(self, bonafide, *parts):
-        return bonafide.do_get_active_user()
-
 
 class EIPCmd(SubCommand):
 
@@ -200,9 +196,11 @@ class EIPCmd(SubCommand):
 
     @register_method('dict')
     def do_START(self, eip, *parts):
-        # TODO --- attempt to get active provider
-        # TODO or catch the exception and send error
-        provider = parts[2]
+        try:
+            provider = parts[2]
+        except IndexError:
+            raise DispatchError(
+                'wrong number of arguments: expected 1, got none')
         d = eip.do_start(provider)
         return d
 
@@ -267,17 +265,15 @@ class KeysCmd(SubCommand):
 
     @register_method("[dict]")
     def do_LIST(self, service, *parts, **kw):
+        if len(parts) < 3:
+            raise ValueError("A uid is needed")
         uid = parts[2]
 
         private = False
         if parts[-1] == 'private':
             private = True
 
-        d = defer.succeed(uid)
-        if not uid:
-            d = self._get_active_user(kw['bonafide'])
-        d.addCallback(service.do_list_keys, private)
-        return d
+        return service.do_list_keys(uid, private)
 
     @register_method('dict')
     def do_EXPORT(self, service, *parts, **kw):
@@ -290,14 +286,9 @@ class KeysCmd(SubCommand):
         if parts[-1] == 'private':
             private = True
 
-        d = defer.succeed(uid)
-        if not uid:
-            d = self._get_active_user(kw['bonafide'])
-        d.addCallback(service.do_export, address, private)
-        return d
+        return service.do_export(uid, address, private)
 
     @register_method('dict')
-    @defer.inlineCallbacks
     def do_INSERT(self, service, *parts, **kw):
         if len(parts) < 6:
             raise ValueError("An email address is needed")
@@ -306,14 +297,9 @@ class KeysCmd(SubCommand):
         validation = parts[4]
         rawkey = parts[5]
 
-        d = defer.succeed(uid)
-        if not uid:
-            d = self._get_active_user(kw['bonafide'])
-        d.addCallback(service.do_insert, address, rawkey, validation)
-        return d
+        return service.do_insert(uid, address, rawkey, validation)
 
     @register_method('str')
-    @defer.inlineCallbacks
     def do_DELETE(self, service, *parts, **kw):
         if len(parts) < 4:
             raise ValueError("An email address is needed")
@@ -324,16 +310,7 @@ class KeysCmd(SubCommand):
         if parts[-1] == 'private':
             private = True
 
-        d = defer.succeed(uid)
-        if not uid:
-            d = self._get_active_user(kw['bonafide'])
-        d.addCallback(service.do_delete, address, private)
-        return d
-
-    def _get_active_user(self, bonafide):
-        d = bonafide.do_get_active_user()
-        d.addCallback(lambda active: active['user'])
-        return d
+        return service.do_delete(uid, address, private)
 
 
 class EventsCmd(SubCommand):
-- 
cgit v1.2.3