diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2013-10-21 08:06:00 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2013-10-21 08:06:00 (GMT) |
commit | 8223b2b538540aa227a30cc18434571455af5bc3 (patch) | |
tree | 1c5c275ff8c6471687335d50aee6666dd2f67412 /sugar_network/client | |
parent | 58471a0a713dd3ef77276a3d0df23c44a5ddeb0d (diff) |
Switch to RSA auth keys; keep private key per user, not per sugar profile
Diffstat (limited to 'sugar_network/client')
-rw-r--r-- | sugar_network/client/__init__.py | 169 | ||||
-rw-r--r-- | sugar_network/client/routes.py | 81 | ||||
-rw-r--r-- | sugar_network/client/solver.py | 10 |
3 files changed, 115 insertions, 145 deletions
diff --git a/sugar_network/client/__init__.py b/sugar_network/client/__init__.py index 6a620f9..d8eefcb 100644 --- a/sugar_network/client/__init__.py +++ b/sugar_network/client/__init__.py @@ -15,42 +15,10 @@ import os import logging +from base64 import b64encode from os.path import join, expanduser, exists -from sugar_network import toolkit -from sugar_network.toolkit import Option - - -SUGAR_API_COMPATIBILITY = { - '0.94': frozenset(['0.86', '0.88', '0.90', '0.92', '0.94']), - } - -_NICKNAME_GCONF = '/desktop/sugar/user/nick' -_COLOR_GCONF = '/desktop/sugar/user/color' -_XO_SERIAL_PATH = ['/ofw/mfg-data/SN', '/proc/device-tree/mfg-data/SN'] -_XO_UUID_PATH = ['/ofw/mfg-data/U#', '/proc/device-tree/mfg-data/U#'] - -_logger = logging.getLogger('client') -_sugar_uid = None - - -def profile_path(*args): - """Path within sugar profile directory. - - Missed directories will be created. - - :param args: - path parts that will be added to the resulting path - :returns: - full path with directory part existed - - """ - if os.geteuid(): - root_dir = join(os.environ['HOME'], '.sugar', - os.environ.get('SUGAR_PROFILE', 'default')) - else: - root_dir = '/var/sugar-network' - return join(root_dir, *args) +from sugar_network.toolkit import http, Option api_url = Option( @@ -68,7 +36,7 @@ no_check_certificate = Option( local_root = Option( 'path to the directory to keep all local data', - default=profile_path('network'), name='local_root') + default=lambda: profile_path('network'), name='local_root') server_mode = Option( 'start server to share local documents', @@ -104,23 +72,6 @@ discover_server = Option( default=False, type_cast=Option.bool_cast, action='store_true', name='discover_server') -no_dbus = Option( - 'disable any DBus usage', - default=False, type_cast=Option.bool_cast, - action='store_true', name='no-dbus') - -anonymous = Option( - 'use anonymous user to access to Sugar Network server; ' - 'only read-only operations are available in this mode', - default=False, type_cast=Option.bool_cast, action='store_true', - name='anonymous') - -accept_language = Option( - 'comma separated list to specify HTTP Accept-Language ' - 'header field value manually', - default=[], type_cast=Option.list_cast, type_repr=Option.list_repr, - name='accept-language', short_option='-l') - cache_limit = Option( 'the minimal disk free space, in bytes, to preserve while recycling ' 'disk cache; the final limit will be a minimal between --cache-limit ' @@ -142,6 +93,44 @@ cache_timeout = Option( 'check disk cache for recycling in specified delay in seconds', default=3600, type_cast=int, name='cache-timeout') +login = Option( + 'Sugar Labs account to connect to Sugar Network API server; ' + 'should be set only if either password is provided or public key ' + 'for Sugar Labs account was uploaded to the Sugar Network', + name='login', short_option='-l') + +password = Option( + 'Sugar Labs account password to connect to Sugar Network API server ' + 'using Basic authentication; if omitted, keys based authentication ' + 'will be used', + name='password', short_option='-p') + +keyfile = Option( + 'path to RSA private key to connect to Sugar Network API server', + name='keyfile', short_option='-k', default='~/.ssh/sugar-network') + + +_logger = logging.getLogger('client') + + +def profile_path(*args): + """Path within sugar profile directory. + + Missed directories will be created. + + :param args: + path parts that will be added to the resulting path + :returns: + full path with directory part existed + + """ + if os.geteuid(): + root_dir = join(os.environ['HOME'], '.sugar', + os.environ.get('SUGAR_PROFILE', 'default')) + else: + root_dir = '/var/sugar-network' + return join(root_dir, *args) + def path(*args): """Calculate a path from the root. @@ -162,36 +151,6 @@ def path(*args): return str(result) -def Connection(url=None): - from sugar_network.toolkit import http - if url is None: - url = api_url.value - creds = None - if not anonymous.value: - if exists(key_path()): - creds = (sugar_uid(), key_path(), sugar_profile) - else: - _logger.warning('Sugar session was never started (no DSA key),' - 'fallback to anonymous mode') - return http.Connection(url, creds=creds) - - -def IPCConnection(): - from sugar_network.toolkit import http - - return http.Connection( - api_url='http://127.0.0.1:%s' % ipc_port.value, - creds=None, - # No need in proxy for localhost - trust_env=False, - # The 1st ipc->client->node request might fail if connection - # to the node is lost, so, initiate the 2nd request from ipc - # to retrive data from client in offline mode without propagating - # errors from ipc - max_retries=1, - ) - - def logger_level(): """Current Sugar logger level as --debug value.""" _LEVELS = { @@ -205,30 +164,6 @@ def logger_level(): return _LEVELS.get(level, 0) -def key_path(): - return profile_path('owner.key') - - -def sugar_uid(): - global _sugar_uid - if _sugar_uid is None: - import hashlib - pubkey = toolkit.pubkey(key_path()).split()[1] - _sugar_uid = str(hashlib.sha1(pubkey).hexdigest()) - return _sugar_uid - - -def sugar_profile(): - import gconf - conf = gconf.client_get_default() - return {'name': conf.get_string(_NICKNAME_GCONF) or '', - 'color': conf.get_string(_COLOR_GCONF) or '#000000,#000000', - 'machine_sn': _read_XO_value(_XO_SERIAL_PATH) or '', - 'machine_uuid': _read_XO_value(_XO_UUID_PATH) or '', - 'pubkey': toolkit.pubkey(key_path()), - } - - def stability(context): value = Option.get('stabilities', context) or \ Option.get('stabilities', 'default') or \ @@ -236,8 +171,20 @@ def stability(context): return value.split() -def _read_XO_value(paths): - for value_path in paths: - if exists(value_path): - with file(value_path) as f: - return f.read().rstrip('\x00\n') +def Connection(url=None, **args): + if url is None: + url = api_url.value + return http.Connection(url, verify=not no_check_certificate.value, **args) + + +def IPCConnection(): + return http.Connection( + api_url='http://127.0.0.1:%s' % ipc_port.value, + # Online ipc->client->node request might fail if node connection + # is lost in client process, so, re-send ipc request immediately + # to retrive data from client in offline mode without propagating + # errors on ipc side + max_retries=1, + # No need in proxy settings to connect to localhost + trust_env=False, + ) diff --git a/sugar_network/client/routes.py b/sugar_network/client/routes.py index 9960b9d..1feedec 100644 --- a/sugar_network/client/routes.py +++ b/sugar_network/client/routes.py @@ -16,6 +16,7 @@ import os import logging import httplib +from base64 import b64encode from zipfile import ZipFile, ZIP_DEFLATED from os.path import join, basename @@ -44,8 +45,7 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes): def __init__(self, home_volume, api_url=None, no_subscription=False): model.FrontRoutes.__init__(self) implementations.Routes.__init__(self, home_volume) - if not client.no_dbus.value: - journal.Routes.__init__(self) + journal.Routes.__init__(self) self._local = _LocalRoutes(home_volume) self._inline = coroutine.Event() @@ -56,6 +56,7 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes): self._no_subscription = no_subscription self._server_mode = not api_url self._api_url = api_url + self._auth = _Auth() if not client.delayed_start.value: self.connect() @@ -63,6 +64,7 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes): def connect(self): self._got_offline(force=True) if self._server_mode: + enforce(not client.login.value) mountpoints.connect(_SN_DIRNAME, self._found_mount, self._lost_mount) else: @@ -113,24 +115,20 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes): # no way to process specified request on the node raise http.ServiceUnavailable() - @route('GET', cmd='status', - mime_type='application/json') - def status(self): - result = {'route': 'proxy' if self._inline.is_set() else 'offline'} - if self._inline.is_set(): - result['node'] = self._node.api_url - return result - @route('GET', cmd='inline', mime_type='application/json') def inline(self): return self._inline.is_set() + @route('GET', cmd='whoami', mime_type='application/json') def whoami(self, request, response): if self._inline.is_set(): - return self.fallback(request, response) + result = self.fallback(request, response) + result['route'] = 'proxy' else: - return {'roles': [], 'guid': client.sugar_uid()} + result = {'roles': [], 'route': 'offline'} + result['guid'] = self._auth.login + return result @route('GET', [None], arguments={ @@ -205,6 +203,7 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes): if client.layers.value and \ request.resource in ('context', 'implementation'): request.add('layer', *client.layers.value) + request.principal = self._auth.login try: reply = self._node.call(request, response) if hasattr(reply, 'read'): @@ -228,11 +227,11 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes): if not force and not self._inline.is_set(): return if self._node is not None: - _logger.debug('Got offline on %r', self._node) self._node.close() if self._inline.is_set(): + _logger.debug('Got offline on %r', self._node) self.broadcast({'event': 'inline', 'state': 'offline'}) - self._inline.clear() + self._inline.clear() self._local.volume.broadcast = self.broadcast def _restart_online(self): @@ -266,9 +265,10 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes): def handshake(url): _logger.debug('Connecting to %r node', url) - self._node = client.Connection(url) - info = self._node.get(cmd='info') - impl_info = info['resources'].get('implementation') + self._node = client.Connection(url, auth=self._auth) + status = self._node.get(cmd='status') + self._auth.allow_basic_auth = (status.get('level') == 'master') + impl_info = status['resources'].get('implementation') if impl_info: self.invalidate_solutions(impl_info['mtime']) if self._inline.is_set(): @@ -317,8 +317,13 @@ class ClientRoutes(model.FrontRoutes, implementations.Routes, journal.Routes): node.data_root.value = db_path node.stats_root.value = join(root, _SN_DIRNAME, 'stats') node.files_root.value = join(root, _SN_DIRNAME, 'files') - volume = db.Volume(db_path, model.RESOURCES) + + if not volume['user'].exists(self._auth.login): + profile = self._auth.profile() + profile['guid'] = self._auth.login + volume['user'].create(profile) + self._node = _NodeRoutes(join(db_path, 'node'), volume, self.broadcast) self._jobs.spawn(volume.populate) @@ -360,7 +365,7 @@ class CachedClientRoutes(ClientRoutes): def push(request, seq): try: - self._node.call(request) + self.fallback(request) except Exception: _logger.exception('Cannot push %r, will postpone', request) skiped_seq.include(seq) @@ -456,21 +461,9 @@ class _NodeRoutes(SlaveRoutes, Router): self._mounts = toolkit.Pool() self._jobs = coroutine.Pool() - users = volume['user'] - if not users.exists(client.sugar_uid()): - profile = client.sugar_profile() - profile['guid'] = client.sugar_uid() - users.create(profile) - mountpoints.connect(_SYNC_DIRNAME, self.__found_mountcb, self.__lost_mount_cb) - def preroute(self, op, request): - request.principal = client.sugar_uid() - - def whoami(self, request, response): - return {'roles': [], 'guid': client.sugar_uid()} - def broadcast(self, event=None, request=None): SlaveRoutes.broadcast(self, event, request) self._localcast(event) @@ -537,3 +530,29 @@ class _ResponseStream(object): except (http.ConnectionError, httplib.IncompleteRead): self._on_fail_cb() raise + + +class _Auth(http.SugarAuth): + + def __init__(self): + http.SugarAuth.__init__(self, client.keyfile.value) + if client.login.value: + self.login = client.login.value + self.allow_basic_auth = False + + def profile(self): + if self.allow_basic_auth and \ + client.login.value and client.password.value: + return None + import gconf + conf = gconf.client_get_default() + self._profile['name'] = conf.get_string('/desktop/sugar/user/nick') + self._profile['color'] = conf.get_string('/desktop/sugar/user/color') + return http.SugarAuth.profile(self) + + def __call__(self, nonce): + if not self.allow_basic_auth or \ + not client.login.value or not client.password.value: + return http.SugarAuth.__call__(self, nonce) + auth = b64encode('%s:%s' % (client.login.value, client.password.value)) + return 'Basic %s' % auth diff --git a/sugar_network/client/solver.py b/sugar_network/client/solver.py index 4d4e341..9f29e1d 100644 --- a/sugar_network/client/solver.py +++ b/sugar_network/client/solver.py @@ -19,7 +19,7 @@ import sys import logging from os.path import isabs, join, dirname -from sugar_network.client import packagekit, SUGAR_API_COMPATIBILITY +from sugar_network.client import packagekit from sugar_network.toolkit.spec import parse_version from sugar_network.toolkit import http, lsb_release @@ -33,6 +33,10 @@ from zeroinstall.injector.arch import machine_ranks from zeroinstall.injector.distro import try_cleanup_distro_version +_SUGAR_API_COMPATIBILITY = { + '0.94': frozenset(['0.86', '0.88', '0.90', '0.92', '0.94']), + } + model.Interface.__init__ = lambda *args: _interface_init(*args) reader.check_readable = lambda *args, **kwargs: True reader.update_from_cache = lambda *args, **kwargs: None @@ -102,7 +106,7 @@ def solve(call, context, stability): if feed.to_resolve: continue if status is None: - status = call(method='GET', cmd='status') + status = call(method='GET', cmd='whoami') if status['route'] == 'offline': raise http.ServiceUnavailable(str(error)) else: @@ -182,7 +186,7 @@ def _load_feed(context): except ImportError: # XXX sweets-sugar binding might be not sourced host_version = '0.94' - for version in SUGAR_API_COMPATIBILITY.get(host_version) or []: + for version in _SUGAR_API_COMPATIBILITY.get(host_version) or []: feed.implement_sugar(version) feed.name = context return feed |