From 8223b2b538540aa227a30cc18434571455af5bc3 Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Mon, 21 Oct 2013 08:06:00 +0000 Subject: Switch to RSA auth keys; keep private key per user, not per sugar profile --- diff --git a/TODO b/TODO index d6b92f5..e234a2f 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,3 @@ -- 0.10 - * keep user guids after reflasing XOs - * "-c" command - - (!) Editors' workflows: - (?) log all (including editros) posters of particular document to minimize conflicts about why somthing was changed or better, detailed log for every editor's change @@ -14,3 +10,4 @@ - revert per-document "downloads" property as "launches", a part of unpersonizalied user_stats - sync node->local db sync - parse command while uploading impls; while parsing, take into accoun quotes +- secure node-to-node offline sync diff --git a/sugar-network b/sugar-network index 0c77de5..7dd66bf 100755 --- a/sugar-network +++ b/sugar-network @@ -35,10 +35,15 @@ from sugar_network.toolkit import application, coroutine from sugar_network.toolkit import Option, BUFFER_SIZE, enforce +quiet = Option( + 'turn off any output', + default=False, type_cast=Option.bool_cast, action='store_true', + name='quiet') + porcelain = Option( 'give the output in an easy-to-parse format for scripts', default=False, type_cast=Option.bool_cast, action='store_true', - short_option='-p', name='porcelain') + short_option='-P', name='porcelain') post_data = Option( 'send content as a string from POST or PUT command', @@ -111,19 +116,6 @@ class Application(application.Application): ipc.get(['context', bundle_id], cmd='launch', **params) @application.command( - 'clone Sugar activities to ~/Activities directory', - args='BUNDLE_ID', - ) - def clone(self): - enforce(self.check_for_instance(), 'No sugar-network-client session') - ipc = IPCConnection() - - enforce(self.args, 'BUNDLE_ID was not specified') - bundle_id = self.args.pop(0) - - ipc.put(['context', bundle_id], 1, cmd='clone') - - @application.command( 'upload new implementaion for a context; if BUNDLE_PATH points ' 'not to a .xo bundle, specify all implementaion PROPERTYs for the ' 'new release (at least context and version)', @@ -140,89 +132,60 @@ class Application(application.Application): value = [i for i in _LIST_RE.split(props['license'].strip()) if i] props['license'] = value - conn = self._connect() - # XXX Have to proceed auth before uploading data - conn.get(cmd='whoami') + if self.check_for_instance(): + conn = IPCConnection() + else: + conn = Connection(client.api_url.value) guid = conn.upload(['implementation'], path, cmd='submit', **props) if porcelain.value: - print guid + self._print(guid, '\n') else: - print '-- Uploaded %s implementaion' % guid + self._print('-- Uploaded %s implementaion' % guid, '\n') @application.command( 'send raw API POST request; ' 'specifies all ARGUMENTs the particular API call requires', args='PATH [ARGUMENT=VALUE]') - def post(self): - self._request('POST', True) - - @application.command(hidden=True) def POST(self): - self.post() + self._request('POST', True, Response()) @application.command( 'send raw API PUT request; ' 'specifies all ARGUMENTs the particular API call requires', args='PATH [ARGUMENT=VALUE]') - def put(self): - self._request('PUT', True) - - @application.command(hidden=True) def PUT(self): - self.put() + self._request('PUT', True, Response()) @application.command( 'send raw API DELETE request', args='PATH') - def delete(self): - self._request('DELETE', False) - - @application.command(hidden=True) def DELETE(self): - self.delete() + self._request('DELETE', False, Response()) @application.command( 'send raw API GET request; ' 'specifies all ARGUMENTs the particular API call requires', args='PATH [ARGUMENT=VALUE]') - def get(self): - self._request('GET', False) - - @application.command(hidden=True) def GET(self): - self.get() + self._request('GET', False, Response()) @application.command( 'send raw API HEAD request; ' 'specifies all ARGUMENTs the particular API call requires', args='PATH [ARGUMENT=VALUE]') - def head(self): - request = Request(method='HEAD') - self._parse_path(request) - self._parse_args(request) + def HEAD(self): response = Response() - self._connect().call(request, response) + self._request('HEAD', False, response) result = {} result.update(response) result.update(response.meta) self._dump(result) - @application.command(hidden=True) - def HEAD(self): - self.head() - - def _connect(self): - if self.check_for_instance(): - return IPCConnection() - else: - return Connection(client.api_url.value) - - def _request(self, method, post): + def _request(self, method, post, response): request = Request(method=method) request.allow_redirects = True request.accept_encoding = '' - response = Response() if post: if post_data.value is None and post_file.value is None: @@ -249,15 +212,12 @@ class Application(application.Application): self._parse_args(request) pid_path = None - server = None cp = None try: if self.check_for_instance(): cp = IPCConnection() else: pid_path = self.new_instance() - if not client.anonymous.value: - toolkit.ensure_key(client.key_path()) cp = ClientRouter() result = cp.call(request, response) @@ -281,12 +241,10 @@ class Application(application.Application): chunk = result.read(BUFFER_SIZE) if not chunk: break - sys.stdout.write(chunk) + self._print(chunk, '\n') else: - sys.stdout.write(result) + self._print(result, '\n') finally: - if server is not None: - server.close() if cp is not None: cp.close() if pid_path: @@ -318,7 +276,7 @@ class Application(application.Application): def _dump(self, result): if not porcelain.value: - print dumps(result, indent=2, ensure_ascii=False) + self._print(dumps(result, indent=2, ensure_ascii=False), '\n') return def porcelain_dump(value): @@ -327,17 +285,17 @@ class Application(application.Application): porcelain_dump(value.values()[0]) else: for i in sorted(value.items()): - print '%-18s%s' % i + self._print('%-18s%s' % i, '\n') else: if type(value) not in (list, tuple): value = [value] for n, i in enumerate(value): if n: - print '\t', + self._print('\t') if type(i) is dict and len(i) == 1: i = i.values()[0] - print i, - print '' + self._print(i) + self._print('\n') if type(result) in (list, tuple): for i in result: @@ -349,6 +307,10 @@ class Application(application.Application): else: porcelain_dump(result) + def _print(self, *data): + if not quiet.value: + print ''.join(data) + # Let toolkit.http work in concurrence monkey.patch_socket() @@ -363,7 +325,7 @@ application.debug.value = client.logger_level() toolkit.cachedir.value = client.profile_path('tmp') Option.seek('main', [ - application.debug, porcelain, post_data, post_file, json, offline, + application.debug, quiet, porcelain, post_data, post_file, json, offline, ]) Option.seek('main', [toolkit.cachedir]) Option.seek('client', client) diff --git a/sugar-network-client b/sugar-network-client index e2f152e..5147168 100755 --- a/sugar-network-client +++ b/sugar-network-client @@ -61,10 +61,6 @@ class Application(application.Daemon): printf.info('%s already started, no need in index', self.name) return - if not exists(client.key_path()): - # Command was launched in foreign environment - client.anonymous.value = True - printf.info('Index database in %r', client.local_root.value) volume = db.Volume(client.path('db'), RESOURCES) @@ -100,7 +96,6 @@ class Application(application.Daemon): self.cmd_start() def run(self): - toolkit.ensure_key(client.key_path()) volume = db.Volume(client.path('db'), RESOURCES, lazy_open=True) routes = CachedClientRoutes(volume, client.api_url.value if not client.server_mode.value else None) @@ -195,8 +190,6 @@ locale.setlocale(locale.LC_ALL, '') # New defaults application.debug.value = client.logger_level() -# It seems to be that most of users (on XO at least) don't have recent SSH -node.trust_users.value = True # If tmpfs is mounted to /tmp, `os.fstat()` will return 0 free space # and will brake offline synchronization logic toolkit.cachedir.value = client.profile_path('tmp') @@ -206,8 +199,7 @@ Option.seek('main', [toolkit.cachedir]) Option.seek('webui', webui) Option.seek('client', client) Option.seek('db', db) -Option.seek('node', [node.port, node.find_limit, node.sync_layers, - node.trust_users]) +Option.seek('node', [node.port, node.find_limit, node.sync_layers]) Option.seek('node-stats', stats_node) Option.seek('user-stats', stats_user) diff --git a/sugar-network-node b/sugar-network-node index ee52b32..ff10c36 100755 --- a/sugar-network-node +++ b/sugar-network-node @@ -77,7 +77,7 @@ class Application(application.Daemon): # XXX Until implementing regular web users from sugar_network.client.routes import ClientRoutes - client.sugar_uid = lambda: 'demo' + client.login.value = 'demo' # Point client API to volume directly client.mounts_root.value = None 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 diff --git a/sugar_network/db/directory.py b/sugar_network/db/directory.py index d27f0e0..86afed2 100644 --- a/sugar_network/db/directory.py +++ b/sugar_network/db/directory.py @@ -16,6 +16,7 @@ import os import shutil import logging +from cStringIO import StringIO from os.path import exists, join from sugar_network import toolkit @@ -319,8 +320,10 @@ class Directory(object): for name, prop in self.metadata.items(): value = changes.get(name) if isinstance(prop, BlobProperty): - if value is not None: + if isinstance(value, dict): record.set(name, seqno=seqno, **value) + elif isinstance(value, basestring): + record.set(name, seqno=seqno, blob=StringIO(value)) elif isinstance(prop, StoredProperty): if value is None: enforce(existed or prop.default is not None, diff --git a/sugar_network/db/routes.py b/sugar_network/db/routes.py index b863b64..79682d1 100644 --- a/sugar_network/db/routes.py +++ b/sugar_network/db/routes.py @@ -321,9 +321,14 @@ def _read_blob(request, prop, value): finally: dst.close() + if request.prop and request.content_type: + mime_type = request.content_type + else: + mime_type = prop.mime_type + return {'blob': dst.name, 'digest': digest.hexdigest(), - 'mime_type': request.content_type or prop.mime_type, + 'mime_type': mime_type, } diff --git a/sugar_network/model/routes.py b/sugar_network/model/routes.py index d36e1c8..17da118 100644 --- a/sugar_network/model/routes.py +++ b/sugar_network/model/routes.py @@ -77,14 +77,6 @@ class FrontRoutes(object): response['Allow'] = 'GET, HEAD, POST, PUT, DELETE' response.content_length = 0 - @route('GET', cmd='whoami', mime_type='application/json') - def whoami(self, request, response): - roles = [] - # pylint: disable-msg=E1101 - if self.authorize(request.principal, 'root'): - roles.append('root') - return {'roles': roles, 'guid': request.principal} - @route('GET', cmd='subscribe', mime_type='text/event-stream') def subscribe(self, request=None, response=None, ping=False, **condition): """Subscribe to Server-Sent Events.""" @@ -93,10 +85,7 @@ class FrontRoutes(object): if response is not None: response.content_type = 'text/event-stream' response['Cache-Control'] = 'no-cache' - peer = 'anonymous' - if hasattr(request, 'environ'): - peer = request.environ.get('HTTP_X_SN_LOGIN') or peer - return self._pull_events(peer, ping, condition) + return self._pull_events(ping, condition) @route('POST', cmd='broadcast', mime_type='application/json', acl=ACL.LOCAL) @@ -129,9 +118,7 @@ class FrontRoutes(object): 'mime_type': 'image/x-icon', }) - def _pull_events(self, peer, ping, condition): - _logger.debug('Start pulling events to %s user', peer) - + def _pull_events(self, ping, condition): if ping: # XXX The whole commands' kwargs handling should be redesigned if 'ping' in condition: @@ -142,19 +129,16 @@ class FrontRoutes(object): # `GET /?cmd=subscribe` call. yield {'event': 'pong'} - try: - while True: - event = self._pooler.wait() - for key, value in condition.items(): - if value.startswith('!'): - if event.get(key) == value[1:]: - break - elif event.get(key) != value: + while True: + event = self._pooler.wait() + for key, value in condition.items(): + if value.startswith('!'): + if event.get(key) == value[1:]: break - else: - yield event - finally: - _logger.debug('Stop pulling events to %s user', peer) + elif event.get(key) != value: + break + else: + yield event class _Pooler(object): diff --git a/sugar_network/model/user.py b/sugar_network/model/user.py index 13c8684..e35cce8 100644 --- a/sugar_network/model/user.py +++ b/sugar_network/model/user.py @@ -27,18 +27,6 @@ class User(db.Resource): def color(self, value): return value - @db.indexed_property(prefix='S', default='', acl=ACL.CREATE | ACL.WRITE) - def machine_sn(self, value): - return value - - @db.indexed_property(prefix='U', default='', acl=ACL.CREATE | ACL.WRITE) - def machine_uuid(self, value): - return value - - @db.stored_property(acl=ACL.CREATE) - def pubkey(self, value): - return value - @db.indexed_property(prefix='P', full_text=True, default='') def location(self, value): return value @@ -46,3 +34,7 @@ class User(db.Resource): @db.indexed_property(slot=2, prefix='B', default=0, typecast=int) def birthday(self, value): return value + + @db.blob_property(acl=ACL.CREATE, mime_type='text/plain') + def pubkey(self, value): + return value diff --git a/sugar_network/node/__init__.py b/sugar_network/node/__init__.py index 68f3e2a..92970d5 100644 --- a/sugar_network/node/__init__.py +++ b/sugar_network/node/__init__.py @@ -33,12 +33,6 @@ certfile = Option( 'path to SSL certificate file to serve requests via HTTPS', name='certfile') -trust_users = Option( - 'switch off user credentials check; disabling this option will ' - 'require OpenSSH-5.6 or later', - default=False, type_cast=Option.bool_cast, - action='store_true', name='trust-users') - data_root = Option( 'path to a directory to place server data', default='/var/lib/sugar-network', name='data_root') diff --git a/sugar_network/node/master.py b/sugar_network/node/master.py index 3d63d2f..a121a4b 100644 --- a/sugar_network/node/master.py +++ b/sugar_network/node/master.py @@ -139,6 +139,11 @@ class MasterRoutes(NodeRoutes): enforce(aliases, http.BadRequest, 'Nothing to presolve') return obs.presolve(aliases, node.files_root.value) + def status(self): + result = NodeRoutes.status(self) + result['level'] = 'master' + return result + def after_post(self, doc): if doc.metadata.name == 'context': shift_implementations = doc.modified('dependencies') diff --git a/sugar_network/node/routes.py b/sugar_network/node/routes.py index bc86799..32ba497 100644 --- a/sugar_network/node/routes.py +++ b/sugar_network/node/routes.py @@ -14,6 +14,7 @@ # along with this program. If not, see . import os +import time import shutil import gettext import logging @@ -26,15 +27,16 @@ from sugar_network import node, toolkit, model from sugar_network.node import stats_node, stats_user from sugar_network.model.context import Context # pylint: disable-msg=W0611 -from sugar_network.toolkit.router import route, preroute, postroute -from sugar_network.toolkit.router import Request, ACL, fallbackroute +from sugar_network.toolkit.router import route, preroute, postroute, ACL +from sugar_network.toolkit.router import Unauthorized, Request, fallbackroute from sugar_network.toolkit.spec import EMPTY_LICENSE from sugar_network.toolkit.spec import parse_requires, ensure_requires from sugar_network.toolkit.bundle import Bundle -from sugar_network.toolkit import http, coroutine, exception, enforce +from sugar_network.toolkit import pylru, http, coroutine, exception, enforce _MAX_STATS_LENGTH = 100 +_AUTH_POOL_SIZE = 1024 _logger = logging.getLogger('node.routes') @@ -48,7 +50,7 @@ class NodeRoutes(model.VolumeRoutes, model.FrontRoutes): self._guid = guid self._stats = None - self._authenticated = set() + self._auth_pool = pylru.lrucache(_AUTH_POOL_SIZE) self._auth_config = None self._auth_config_mtime = 0 @@ -60,14 +62,19 @@ class NodeRoutes(model.VolumeRoutes, model.FrontRoutes): def guid(self): return self._guid - @route('GET', cmd='status', - mime_type='application/json') - def status(self): - return {'route': 'direct'} + @route('GET', cmd='logon', acl=ACL.AUTH) + def logon(self): + pass - @route('GET', cmd='info', - mime_type='application/json') - def info(self): + @route('GET', cmd='whoami', mime_type='application/json') + def whoami(self, request, response): + roles = [] + if self.authorize(request.principal, 'root'): + roles.append('root') + return {'roles': roles, 'guid': request.principal, 'route': 'direct'} + + @route('GET', cmd='status', mime_type='application/json') + def status(self): documents = {} for name, directory in self.volume.items(): documents[name] = {'mtime': directory.mtime} @@ -109,6 +116,11 @@ class NodeRoutes(model.VolumeRoutes, model.FrontRoutes): return result + @route('POST', ['user'], mime_type='application/json') + def register(self, request): + # To avoid authentication while registering new user + self.create(request) + @fallbackroute('GET', ['packages']) def route_packages(self, request, response): enforce(node.files_root.value, http.BadRequest, 'Disabled') @@ -252,30 +264,31 @@ class NodeRoutes(model.VolumeRoutes, model.FrontRoutes): return '' @preroute - def preroute(self, op, request): - user = request.environ.get('HTTP_X_SN_LOGIN') - if user and user not in self._authenticated and \ - (request.path != ['user'] or request.method != 'POST'): - _logger.debug('Logging %r user', user) - enforce(self.volume['user'].exists(user), http.Unauthorized, - 'Principal does not exist') - # TODO Process X-SN-signature - self._authenticated.add(user) - request.principal = user - - if op.acl & ACL.AUTH: - enforce(self.authorize(user, 'user'), http.Unauthorized, - 'User is not authenticated') + def preroute(self, op, request, response): + if op.acl & ACL.AUTH and request.principal is None: + if not request.authorization: + enforce(self.authorize(None, 'user'), + Unauthorized, 'No credentials') + else: + if request.authorization not in self._auth_pool: + self.authenticate(request.authorization) + self._auth_pool[request.authorization] = True + enforce(not request.authorization.nonce or + request.authorization.nonce >= time.time(), + Unauthorized, 'Credentials expired') + request.principal = request.authorization.login + if op.acl & ACL.AUTHOR and request.guid: if request.resource == 'user': - allowed = (user == request.guid) + allowed = (request.principal == request.guid) else: doc = self.volume[request.resource].get(request.guid) - allowed = (user in doc['author']) - enforce(allowed or self.authorize(user, 'root'), + allowed = (request.principal in doc['author']) + enforce(allowed or self.authorize(request.principal, 'root'), http.Forbidden, 'Operation is permitted only for authors') + if op.acl & ACL.SUPERUSER: - enforce(self.authorize(user, 'root'), http.Forbidden, + enforce(self.authorize(request.principal, 'root'), http.Forbidden, 'Operation is permitted only for superusers') @postroute @@ -286,7 +299,8 @@ class NodeRoutes(model.VolumeRoutes, model.FrontRoutes): def on_create(self, request, props, event): if request.resource == 'user': - props['guid'], props['pubkey'] = _load_pubkey(props['pubkey']) + with file(props['pubkey']['blob']) as f: + props['guid'] = str(hashlib.sha1(f.read()).hexdigest()) model.VolumeRoutes.on_create(self, request, props, event) def on_update(self, request, props, event): @@ -315,6 +329,19 @@ class NodeRoutes(model.VolumeRoutes, model.FrontRoutes): 'Resource deleted') return model.VolumeRoutes.get(self, request, reply) + def authenticate(self, auth): + enforce(auth.scheme == 'sugar', http.BadRequest, + 'Unknown authentication scheme') + if not self.volume['user'].exists(auth.login): + raise Unauthorized('Principal does not exist', auth.nonce) + + from M2Crypto import RSA + + data = hashlib.sha1('%s:%s' % (auth.login, auth.nonce)).digest() + key = RSA.load_pub_key(self.volume['user'].path(auth.login, 'pubkey')) + enforce(key.verify(data, auth.signature.decode('hex')), + http.Forbidden, 'Bad credentials') + def authorize(self, user, role): if role == 'user' and user: return True @@ -496,26 +523,3 @@ def _load_context_metadata(bundle, spec): exception(_logger, 'Gettext failed to read %r', mo_path[-1]) return result - - -def _load_pubkey(pubkey): - pubkey = pubkey.strip() - try: - with toolkit.NamedTemporaryFile() as key_file: - key_file.file.write(pubkey) - key_file.file.flush() - # SSH key needs to be converted to PKCS8 to ket M2Crypto read it - pubkey_pkcs8 = toolkit.assert_call( - ['ssh-keygen', '-f', key_file.name, '-e', '-m', 'PKCS8']) - except Exception: - message = 'Cannot read DSS public key gotten for registeration' - exception(_logger, message) - if node.trust_users.value: - logging.warning('Failed to read registration pubkey, ' - 'but we trust users') - # Keep SSH key for further converting to PKCS8 - pubkey_pkcs8 = pubkey - else: - raise http.Forbidden(message) - - return str(hashlib.sha1(pubkey.split()[1]).hexdigest()), pubkey_pkcs8 diff --git a/sugar_network/node/slave.py b/sugar_network/node/slave.py index 2ad1d5a..69584be 100644 --- a/sugar_network/node/slave.py +++ b/sugar_network/node/slave.py @@ -35,10 +35,9 @@ _logger = logging.getLogger('node.slave') class SlaveRoutes(NodeRoutes): def __init__(self, key_path, volume_): - guid = toolkit.ensure_key(key_path) - NodeRoutes.__init__(self, guid, volume_) + self._auth = http.SugarAuth(key_path) + NodeRoutes.__init__(self, self._auth.login, volume_) - self._key_path = key_path self._push_seq = toolkit.PersistentSequence( join(volume_.root, 'push.sequence'), [1, None]) self._pull_seq = toolkit.PersistentSequence( @@ -50,19 +49,11 @@ class SlaveRoutes(NodeRoutes): @route('POST', cmd='online-sync', acl=ACL.LOCAL) def online_sync(self, no_pull=False): - profile = { - 'name': self.guid, - 'color': '#000000,#000000', - 'machine_sn': '', - 'machine_uuid': '', - 'pubkey': toolkit.pubkey(self._key_path), - } - conn = http.Connection(api_url.value, - creds=(self.guid, self._key_path, lambda: profile)) - - # TODO In case if slave user is not created on master - # `http.Connection` should handle re-POSTing without loosing payload - conn.get(cmd='whoami') + conn = http.Connection(api_url.value, auth=self._auth) + + # TODO `http.Connection` should handle re-POSTing without + # loosing payload after authentication + conn.get(cmd='logon') push = [('diff', None, volume.diff(self.volume, self._push_seq))] if not no_pull: @@ -108,6 +99,11 @@ class SlaveRoutes(NodeRoutes): _logger.debug('Postpone synchronization with %r session', self._offline_session) + def status(self): + result = NodeRoutes.status(self) + result['level'] = 'slave' + return result + def _offline_sync(self, path, push_seq=None, stats_seq=None, session=None): push = [] diff --git a/sugar_network/toolkit/__init__.py b/sugar_network/toolkit/__init__.py index c805b06..fee7897 100644 --- a/sugar_network/toolkit/__init__.py +++ b/sugar_network/toolkit/__init__.py @@ -267,31 +267,6 @@ def init_logging(debug_level=None, **kwargs): **kwargs) -def ensure_key(path): - import hashlib - if not exists(path): - if 'SSH_ASKPASS' in os.environ: - # Otherwise ssh-keygen will popup auth dialogs on registeration - del os.environ['SSH_ASKPASS'] - if not exists(dirname(path)): - os.makedirs(dirname(path)) - _logger.info('Create DSA key') - assert_call([ - '/usr/bin/ssh-keygen', '-q', '-t', 'dsa', '-f', path, - '-C', '', '-N', '']) - key = pubkey(path).split()[1] - return str(hashlib.sha1(key).hexdigest()) - - -def pubkey(path): - with file(path + '.pub') as f: - for line in f.readlines(): - line = line.strip() - if line.startswith('ssh-'): - return line - raise RuntimeError('No valid DSA public keys in %r' % path) - - def iter_file(*path): with file(join(*path), 'rb') as f: while True: diff --git a/sugar_network/toolkit/coroutine.py b/sugar_network/toolkit/coroutine.py index 8d0c8aa..1e73c0b 100644 --- a/sugar_network/toolkit/coroutine.py +++ b/sugar_network/toolkit/coroutine.py @@ -60,6 +60,7 @@ def shutdown(): def reset_resolver(): + _logger.debug('Reset resolver') gevent.get_hub().resolver = None diff --git a/sugar_network/toolkit/http.py b/sugar_network/toolkit/http.py index 2780c1b..0a25c57 100644 --- a/sugar_network/toolkit/http.py +++ b/sugar_network/toolkit/http.py @@ -13,13 +13,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os import sys import json import types +import hashlib import logging -from os.path import join, dirname +from os.path import join, dirname, exists, expanduser, abspath -from sugar_network import client, toolkit +from sugar_network import toolkit from sugar_network.toolkit import enforce @@ -67,7 +69,6 @@ class BadRequest(Status): class Unauthorized(Status): status = '401 Unauthorized' - headers = {'WWW-Authenticate': 'Sugar'} status_code = 401 @@ -104,16 +105,15 @@ class GatewayTimeout(Status): class Connection(object): _Session = None - _SSLError = None _ConnectionError = None - def __init__(self, api_url='', creds=None, trust_env=True, max_retries=0): + def __init__(self, api_url='', auth=None, max_retries=0, **session_args): self.api_url = api_url - self._get_profile = None - self._session = None - self._creds = creds - self._trust_env = trust_env + self.auth = auth self._max_retries = max_retries + self._session_args = session_args + self._session = None + self._nonce = None def __repr__(self): return '' % self.api_url @@ -199,35 +199,24 @@ class Connection(object): path = [''] if not isinstance(path, basestring): path = '/'.join([i.strip('/') for i in [self.api_url] + path]) - if isinstance(params, basestring): - path += '?' + params - params = None - a_try = 0 + try_ = 0 while True: - a_try += 1 + try_ += 1 try: reply = self._session.request(method, path, data=data, headers=headers, params=params, **kwargs) - except Connection._SSLError: - _logger.warning('Use --no-check-certificate to avoid checks') - raise except Connection._ConnectionError, error: raise ConnectionError, error, sys.exc_info()[2] - if reply.status_code != 200: - if reply.status_code == 401: - enforce(method not in ('PUT', 'POST') or - not hasattr(data, 'read'), - 'Cannot resend data after authentication') - enforce(self._get_profile is not None, - 'Operation is not available in anonymous mode') - _logger.info('User is not registered on the server, ' - 'registering') - self.post(['user'], self._get_profile()) - a_try = 0 - continue - if allowed and reply.status_code in allowed: - break + + if reply.status_code == Unauthorized.status_code: + enforce(self.auth is not None, Unauthorized, 'No credentials') + self._authenticate(reply.headers.get('www-authenticate')) + try_ = 0 + elif reply.status_code == 200 or \ + allowed and reply.status_code in allowed: + break + else: content = reply.content try: error = json.loads(content)['error'] @@ -236,13 +225,12 @@ class Connection(object): # was not sent by the application level server code, i.e., # something happaned on low level, like connection abort. # If so, try to resend request. - if a_try <= self._max_retries and method == 'GET': + if try_ <= self._max_retries and method in ('GET', 'HEAD'): continue error = content or reply.headers.get('x-sn-error') or \ 'No error message provided' cls = _FORWARD_STATUSES.get(reply.status_code, RuntimeError) - raise cls, error, sys.exc_info()[2] - break + raise cls(error) return reply @@ -264,14 +252,12 @@ class Connection(object): else: request.content = request.content_stream.read() headers['content-length'] = str(len(request.content)) - for env_key, key, value in ( - ('HTTP_IF_MODIFIED_SINCE', 'if-modified-since', None), - ('HTTP_ACCEPT_LANGUAGE', 'accept-language', ','.join( - client.accept_language.value or toolkit.default_langs())), - ('HTTP_ACCEPT_ENCODING', 'accept-encoding', None), + for env_key, key in ( + ('HTTP_IF_MODIFIED_SINCE', 'if-modified-since'), + ('HTTP_ACCEPT_LANGUAGE', 'accept-language'), + ('HTTP_ACCEPT_ENCODING', 'accept-encoding'), ): - if value is None: - value = request.environ.get(env_key) + value = request.environ.get(env_key) if value is not None: headers[key] = value @@ -316,25 +302,100 @@ class Connection(object): def _init(self): if Connection._Session is None: - sys.path.insert(0, - join(dirname(__file__), '..', 'lib', 'requests')) - from requests import Session - from requests.exceptions import SSLError - from requests.exceptions import ConnectionError as _ConnectionError + sys_path = join(dirname(dirname(__file__)), 'lib', 'requests') + sys.path.insert(0, sys_path) + from requests import Session, exceptions Connection._Session = Session - Connection._SSLError = SSLError - Connection._ConnectionError = _ConnectionError + Connection._ConnectionError = exceptions.ConnectionError self._session = Connection._Session() + self._session.headers['accept-language'] = \ + ','.join(toolkit.default_langs()) + for arg, value in self._session_args.items(): + setattr(self._session, arg, value) self._session.stream = True - self._session.trust_env = self._trust_env - if client.no_check_certificate.value: - self._session.verify = False - if self._creds: - uid, keyfile, self._get_profile = self._creds - self._session.headers['X-SN-login'] = uid - self._session.headers['X-SN-signature'] = _sign(keyfile, uid) - self._session.headers['accept-language'] = toolkit.default_lang() + + def _authenticate(self, challenge): + from urllib2 import parse_http_list, parse_keqv_list + + nonce = None + if challenge: + challenge = challenge.split(' ', 1)[-1] + nonce = parse_keqv_list(parse_http_list(challenge)).get('nonce') + + if self._nonce and nonce == self._nonce: + enforce(self.auth.profile(), Unauthorized, 'Bad credentials') + _logger.info('Register on the server') + self.post(['user'], self.auth.profile()) + + self._session.headers['authorization'] = self.auth(nonce) + self._nonce = nonce + + +class SugarAuth(object): + + def __init__(self, key_path, profile=None): + self._key_path = abspath(expanduser(key_path)) + self._profile = profile or {'color': '#000000,#000000'} + self._key = None + self._pubkey = None + self._login = None + + @property + def pubkey(self): + if self._pubkey is None: + self.ensure_key() + from M2Crypto.BIO import MemoryBuffer + buf = MemoryBuffer() + self._key.save_pub_key_bio(buf) + self._pubkey = buf.getvalue() + return self._pubkey + + @property + def login(self): + if self._login is None: + self._login = str(hashlib.sha1(self.pubkey).hexdigest()) + return self._login + + def profile(self): + if 'name' not in self._profile: + self._profile['name'] = self.login + self._profile['pubkey'] = self.pubkey + return self._profile + + def __call__(self, nonce): + self.ensure_key() + data = hashlib.sha1('%s:%s' % (self.login, nonce)).digest() + signature = self._key.sign(data).encode('hex') + return 'Sugar username="%s",nonce="%s",signature="%s"' % \ + (self.login, nonce, signature) + + def ensure_key(self): + from M2Crypto import RSA + from base64 import b64encode + + if exists(self._key_path): + self._key = RSA.load_key(self._key_path) + return + + key_dir = dirname(self._key_path) + if not exists(key_dir): + os.makedirs(key_dir) + os.chmod(key_dir, 0700) + + _logger.info('Generate RSA private key at %r', self._key_path) + self._key = RSA.gen_key(2048, 65537, lambda *args: None) + self._key.save_key(self._key_path, cipher=None) + os.chmod(self._key_path, 0600) + + pub_key_path = self._key_path + '.pub' + with file(pub_key_path, 'w') as f: + f.write('ssh-rsa %s %s@%s' % ( + b64encode('\x00\x00\x00\x07ssh-rsa%s%s' % self._key.pub()), + self.login, + os.uname()[1], + )) + _logger.info('Saved RSA public key at %r', pub_key_path) class _Subscription(object): @@ -355,14 +416,14 @@ class _Subscription(object): return self._handshake(ping=True)._fp.fp.fileno() def pull(self): - for a_try in (1, 0): + for try_ in (1, 0): stream = self._handshake() try: line = toolkit.readline(stream) enforce(line, 'Subscription aborted') break except Exception: - if a_try == 0: + if try_ == 0: raise toolkit.exception('Failed to read from %r subscription, ' 'will resubscribe', self._client.api_url) @@ -398,14 +459,6 @@ def _parse_event(line): _logger.exception('Failed to parse %r event', line) -def _sign(key_path, data): - import hashlib - from M2Crypto import DSA - key = DSA.load_key(key_path) - # pylint: disable-msg=E1121 - return key.sign_asn1(hashlib.sha1(data).digest()).encode('hex') - - _FORWARD_STATUSES = { BadRequest.status_code: BadRequest, Forbidden.status_code: Forbidden, diff --git a/sugar_network/toolkit/router.py b/sugar_network/toolkit/router.py index 574b1ef..b6d4466 100644 --- a/sugar_network/toolkit/router.py +++ b/sugar_network/toolkit/router.py @@ -16,10 +16,12 @@ import os import cgi import json +import time import types import logging import calendar import mimetypes +from base64 import b64decode from bisect import bisect_left from urllib import urlencode from urlparse import parse_qsl, urlsplit @@ -30,9 +32,11 @@ from sugar_network import toolkit from sugar_network.toolkit import http, coroutine, enforce -_logger = logging.getLogger('router') +_SIGNATURE_LIFETIME = 600 _NOT_SET = object() +_logger = logging.getLogger('router') + def route(method, path=None, cmd=None, **kwargs): if path is None: @@ -95,6 +99,15 @@ class ACL(object): } +class Unauthorized(http.Unauthorized): + + def __init__(self, message, nonce=None): + http.Unauthorized.__init__(self, message) + if not nonce: + nonce = int(time.time()) + _SIGNATURE_LIFETIME + self.headers = {'www-authenticate': 'Sugar nonce="%s"' % nonce} + + class Request(dict): principal = None @@ -109,13 +122,14 @@ class Request(dict): self.cmd = None self.environ = {} self.session = session or {} - self._content = _NOT_SET + self._content = _NOT_SET self._dirty_query = False self._if_modified_since = _NOT_SET self._accept_language = _NOT_SET self._content_stream = content_stream or _NOT_SET self._content_type = content_type or _NOT_SET + self._authorization = _NOT_SET if environ: url = environ.get('PATH_INFO', '').strip('/') @@ -268,6 +282,28 @@ class Request(dict): self._dirty_query = False return self.environ.get('QUERY_STRING') + @property + def authorization(self): + if self._authorization is _NOT_SET: + auth = self.environ.get('HTTP_AUTHORIZATION') + if not auth: + self._authorization = None + else: + auth = self._authorization = _Authorization(auth) + auth.scheme, creds = auth.strip().split(' ', 1) + auth.scheme = auth.scheme.lower() + if auth.scheme == 'basic': + auth.login, auth.password = b64decode(creds).split(':') + elif auth.scheme == 'sugar': + from urllib2 import parse_http_list, parse_keqv_list + creds = parse_keqv_list(parse_http_list(creds)) + auth.login = creds['username'] + auth.signature = creds['signature'] + auth.nonce = int(creds['nonce']) + else: + raise http.BadRequest('Unsupported authentication scheme') + return self._authorization + def add(self, key, *values): existing_value = self.get(key) for value in values: @@ -284,8 +320,8 @@ class Request(dict): 'HTTP_ACCEPT_LANGUAGE', 'HTTP_ACCEPT_ENCODING', 'HTTP_IF_MODIFIED_SINCE', - 'HTTP_X_SN_LOGIN', - 'HTTP_X_SN_SIGNATURE'): + 'HTTP_AUTHORIZATION', + ): if key in self.environ: environ[key] = self.environ[key] request = Request(environ, **kwargs) @@ -578,7 +614,7 @@ class Router(object): kwargs[arg] = request.get(arg) for i in self._preroutes: - i(route_, request) + i(route_, request, response) result = None exception = None try: @@ -820,3 +856,11 @@ class _Route(object): if self.cmd: path += ('?cmd=%s' % self.cmd) return '%s /%s (%s)' % (self.method, path, self.callback.__name__) + + +class _Authorization(str): + scheme = None + login = None + password = None + signature = None + nonce = None diff --git a/tests/__init__.py b/tests/__init__.py index 84df9b3..f216936 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -19,7 +19,7 @@ from gevent import monkey from sugar_network.toolkit import coroutine, http, mountpoints, Option, gbus from sugar_network.toolkit.router import Router from sugar_network.client import IPCConnection, journal, routes as client_routes -from sugar_network.client.routes import ClientRoutes +from sugar_network.client.routes import ClientRoutes, _Auth from sugar_network import db, client, node, toolkit, model from sugar_network.client import solver from sugar_network.model.user import User @@ -56,7 +56,9 @@ class Test(unittest.TestCase): self.node_volume = None os.environ['LANG'] = 'en_US' + os.environ['LANGUAGE'] = 'en_US' toolkit._default_lang = 'en-us' + toolkit._default_langs = None global tmpdir tmpdir = join(tmp_root or tmproot, '.'.join(self.id().split('.')[1:])) @@ -72,8 +74,6 @@ class Test(unittest.TestCase): os.environ['LC_ALL'] = 'en_US.UTF-8' profile_dir = join(tmpdir, '.sugar', 'default') os.makedirs(profile_dir) - shutil.copy(join(root, 'data', 'owner.key'), join(profile_dir, 'owner.key')) - shutil.copy(join(root, 'data', 'owner.key.pub'), profile_dir) adapters.DEFAULT_RETRIES = 5 Option.items = {} @@ -87,6 +87,8 @@ class Test(unittest.TestCase): node.data_root.value = tmpdir node.files_root.value = None node.sync_layers.value = None + node.stats_root.value = tmpdir + '/stats' + node.port.value = 8880 db.index_write_queue.value = 10 client.local_root.value = tmpdir client.api_url.value = 'http://127.0.0.1:8888' @@ -96,11 +98,11 @@ class Test(unittest.TestCase): client.cache_limit.value = 0 client.cache_limit_percent.value = 0 client.cache_lifetime.value = 0 + client.keyfile.value = join(root, 'data', UID) client_routes._RECONNECT_TIMEOUT = 0 mountpoints._connects.clear() mountpoints._found.clear() mountpoints._COMPLETE_MOUNT_TIMEOUT = .1 - node.stats_root.value = tmpdir + '/stats' stats_node.stats_node.value = False stats_node.stats_node_step.value = 0 stats_user.stats_user.value = False @@ -126,11 +128,9 @@ class Test(unittest.TestCase): ] if tmp_root is None: - self.override(client, 'sugar_profile', lambda: { + self.override(_Auth, 'profile', lambda self: { 'name': 'test', 'color': '#000000,#000000', - 'machine_sn': '', - 'machine_uuid': '', 'pubkey': PUBKEY, }) @@ -392,42 +392,21 @@ def sign(privkey, data): PUBKEY = """\ -ssh-dss AAAAB3NzaC1kc3MAAACBANuYoFH3uvJGoQFMeW6M3CCJQlPrSv6sqd9dGQlwnnNxLBrq6KgY63e10ULtyYzq9UjiIUowqbtheGrtPCtL5w7qmFcCnq1cFzAk6Xxfe6ytJDx1fql5Y1wKqa+zxOKF6SGNnglyxvf78mZXt2G6wx22AjW+1fEhAOr+g8kRiUbBAAAAFQDA/W3LfD5NBB4vlZFcT10jU4B8QwAAAIBHh1U2B71memu/TsatwOo9+CyUyvF0FHHsXwQDkeRjqY3dcfeV38YoU/EbOZtHIQgdfGrzy7m5osnpBwUtHLunZJuwCt5tBNrpU8CAF7nEXOJ4n2FnoNiWO1IsbWdhkh9Hd7+TBM9hLGmOqlqTIx3TmUG0e4F2X33VVJ8UsrJ3mwAAAIEAm29WVw9zkRbv6CTFhPlLjJ71l/2GE9XFbdznJFRmPNBBWF2J452okRWywzeDMIIoi/z0wmNSr2B6P9wduxSxp8eIWQhKVQa4V4lJyqX/A2tE5SQtFULtw3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgE= -""" -INVALID_PUBKEY = """\ -ssh-dss ____B3NzaC1kc3MAAACBANuYoFH3uvJGoQFMeW6M3CCJQlPrSv6sqd9dGQlwnnNxLBrq6KgY63e10ULtyYzq9UjiIUowqbtheGrtPCtL5w7qmFcCnq1cFzAk6Xxfe6ytJDx1fql5Y1wKqa+zxOKF6SGNnglyxvf78mZXt2G6wx22AjW+1fEhAOr+g8kRiUbBAAAAFQDA/W3LfD5NBB4vlZFcT10jU4B8QwAAAIBHh1U2B71memu/TsatwOo9+CyUyvF0FHHsXwQDkeRjqY3dcfeV38YoU/EbOZtHIQgdfGrzy7m5osnpBwUtHLunZJuwCt5tBNrpU8CAF7nEXOJ4n2FnoNiWO1IsbWdhkh9Hd7+TBM9hLGmOqlqTIx3TmUG0e4F2X33VVJ8UsrJ3mwAAAIEAm29WVw9zkRbv6CTFhPlLjJ71l/2GE9XFbdznJFRmPNBBWF2J452okRWywzeDMIIoi/z0wmNSr2B6P9wduxSxp8eIWQhKVQa4V4lJyqX/A2tE5SQtFULtw3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgE= +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9VocIcz6dSUj64ErftV13lne0 +er++oFy17pQXlViwnHIRi4pQutJcJchezLnLxAtDBLE3CsXdQ5RJlMuW7tb9Jt72 +gaN7JMte6f4sKJRBW5rafVewwxzLAw0pFKXqYxQaWEdzOWP2YBbJYuLF2ZB/ZddP +MseM2sIevEeOLXznuwIDAQAB +-----END PUBLIC KEY----- """ -PRIVKEY = """\ ------BEGIN DSA PRIVATE KEY----- -MIIBvAIBAAKBgQDbmKBR97ryRqEBTHlujNwgiUJT60r+rKnfXRkJcJ5zcSwa6uio -GOt3tdFC7cmM6vVI4iFKMKm7YXhq7TwrS+cO6phXAp6tXBcwJOl8X3usrSQ8dX6p -eWNcCqmvs8TihekhjZ4Jcsb3+/JmV7dhusMdtgI1vtXxIQDq/oPJEYlGwQIVAMD9 -bct8Pk0EHi+VkVxPXSNTgHxDAoGAR4dVNge9Znprv07GrcDqPfgslMrxdBRx7F8E -A5HkY6mN3XH3ld/GKFPxGzmbRyEIHXxq88u5uaLJ6QcFLRy7p2SbsArebQTa6VPA -gBe5xFzieJ9hZ6DYljtSLG1nYZIfR3e/kwTPYSxpjqpakyMd05lBtHuBdl991VSf -FLKyd5sCgYEAm29WVw9zkRbv6CTFhPlLjJ71l/2GE9XFbdznJFRmPNBBWF2J452o -kRWywzeDMIIoi/z0wmNSr2B6P9wduxSxp8eIWQhKVQa4V4lJyqX/A2tE5SQtFULt -w3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgECFQC6wU/U -6uUSSSw8Apr+eJQlSFhA+Q== ------END DSA PRIVATE KEY----- -""" -UID = '25c081e29242cf7a19ae893a420ab3de56e9e989' +UID = 'f470db873b6a35903aca1f2492188e1c4b9ffc42' PUBKEY2 = """\ -ssh-dss AAAAB3NzaC1kc3MAAACBAOTS+oSz5nmXlxGLhnadTHwZDf9H124rRLqIxmLhHZy/I93LPHfG1T/hSF9n46DEKwfpLZ8EMNl2VNATvPhbst0ckcsdaB6FSblYVNMFu9C+SAwiX1+JYw8e9koFq8tIKyBz+V1zzr3VUJoUozYvT4MehIFq2YlYR4AdlnfbwQG/AAAAFQDa4fpL/eMJBgp2azVvcHPXoAN1dQAAAIAM41xtZbZ2GvOyiMB49gPFta/SWsie84agasvDVaUljj4RLgIHAOe75V3vh8Myjz7WxBMqS09IRKO8EM9Xv/BeRdLQfXRFvOY3kG4C5EJPIoZykDKCag9fEtw3PMSSf50wvnO0zz1FlJOKsf0tNYfeO98KY3fUNyxoI4p7HbLAoQAAAIEAxHnjr34jnPHGL8n4lhALJDbBUJOP5SwubArF94wodPmFtDI0ia6lWV1o3aHtwpTKRIiyUocJRaTJzxArdSh3jfutxaoIs+KqPgGa3rO5jbHv07b40bpueH8nnb6Mc5Qas/NaCLwWqWoVs5F7w28v70LB88PcmGxxjP1bXxLlDKE= -""" -PRIVKEY2 = """\ ------BEGIN DSA PRIVATE KEY----- -MIIBuwIBAAKBgQDk0vqEs+Z5l5cRi4Z2nUx8GQ3/R9duK0S6iMZi4R2cvyPdyzx3 -xtU/4UhfZ+OgxCsH6S2fBDDZdlTQE7z4W7LdHJHLHWgehUm5WFTTBbvQvkgMIl9f -iWMPHvZKBavLSCsgc/ldc8691VCaFKM2L0+DHoSBatmJWEeAHZZ328EBvwIVANrh -+kv94wkGCnZrNW9wc9egA3V1AoGADONcbWW2dhrzsojAePYDxbWv0lrInvOGoGrL -w1WlJY4+ES4CBwDnu+Vd74fDMo8+1sQTKktPSESjvBDPV7/wXkXS0H10RbzmN5Bu -AuRCTyKGcpAygmoPXxLcNzzEkn+dML5ztM89RZSTirH9LTWH3jvfCmN31DcsaCOK -ex2ywKECgYEAxHnjr34jnPHGL8n4lhALJDbBUJOP5SwubArF94wodPmFtDI0ia6l -WV1o3aHtwpTKRIiyUocJRaTJzxArdSh3jfutxaoIs+KqPgGa3rO5jbHv07b40bpu -eH8nnb6Mc5Qas/NaCLwWqWoVs5F7w28v70LB88PcmGxxjP1bXxLlDKECFFHbJZ6Y -D+YxdWZ851uNEXjVIvza ------END DSA PRIVATE KEY----- +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQ6AWYEKkp5wjPLXd5d024JWPf +ZJ3F9VuFIWNlLMNvGv5XOIAA/VK/tc98Bt6WxI7QZoLEWKb8S4aqkD1KSqjQIpO7 +n9WC2r5B15uTNa1Ry3eq0Z3KGeeC6q0466ETDUhqV03K1quLzR//dGdnBgb+hznL +oLqnwHwnk4DFkdO7ZwIDAQAB +-----END PUBLIC KEY----- """ -UID2 = 'd87dc9fde73fa1cf86c1e7ce86129eaf88985828' +UID2 = 'd820a3405d6aadf2cf207f6817db2a79f8fa07aa' diff --git a/tests/data/d820a3405d6aadf2cf207f6817db2a79f8fa07aa b/tests/data/d820a3405d6aadf2cf207f6817db2a79f8fa07aa new file mode 100644 index 0000000..f3d7365 --- /dev/null +++ b/tests/data/d820a3405d6aadf2cf207f6817db2a79f8fa07aa @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDQ6AWYEKkp5wjPLXd5d024JWPfZJ3F9VuFIWNlLMNvGv5XOIAA +/VK/tc98Bt6WxI7QZoLEWKb8S4aqkD1KSqjQIpO7n9WC2r5B15uTNa1Ry3eq0Z3K +GeeC6q0466ETDUhqV03K1quLzR//dGdnBgb+hznLoLqnwHwnk4DFkdO7ZwIDAQAB +AoGBAIFdrVkluBwCvinN9hZ6Et7eCKLmI+3aqvoeWvbkaYH8LVTF4D68oOmnJP2h +I2YqAmZ7lK8SVxYVjydAezPuV+qtI13J+vDQQxNWWLBzx5v/nyMuXhTraEye+Ia1 +BZXjGF5YTxZCfln5UH1iKOU7C7oNBlshjmnl4xi710XrVXMBAkEA9GxuclWo0gVz +l5nPTJ3pWz1Rpg8qjTIOb125d9IeQ3d6Yi0fi4cHlKwHqt2lzL7b6oTz0fsUS9M8 +smr3FCdVdwJBANrM9C1k1hPmc8KuaFeGGVEfbwlVx+kszOXbPsYjKTchasfg15cc +dvNnXD4rZHKNL+4ACEC8LFUJ0Mvj/QBnBZECQQDjHlJumVv6c6haSUjP17f72Pmi +hVszwjyZjQyG407NwBqPWYCDZSBNLKEfS0ACBaNRwuLQzi6o2or++tfF+Vq3AkEA +yFhM6Vfgsh4dN1ENl77hZaebu3eBcqAkCgCkTTca4TFHHjhiECrvPFcTXpXLBx0C +mzIKXIQxL7iqKFfdbUXoEQJACyIREe0YvHcsbDTrzjL4FQBXqMPAQAVQ4bftDXLx +VVo48USuYAvCi9UWEf6dBHlN94tFn4iFU3kwnAZ7BG0SaA== +-----END RSA PRIVATE KEY----- diff --git a/tests/data/f470db873b6a35903aca1f2492188e1c4b9ffc42 b/tests/data/f470db873b6a35903aca1f2492188e1c4b9ffc42 new file mode 100644 index 0000000..dd838c9 --- /dev/null +++ b/tests/data/f470db873b6a35903aca1f2492188e1c4b9ffc42 @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC9VocIcz6dSUj64ErftV13lne0er++oFy17pQXlViwnHIRi4pQ +utJcJchezLnLxAtDBLE3CsXdQ5RJlMuW7tb9Jt72gaN7JMte6f4sKJRBW5rafVew +wxzLAw0pFKXqYxQaWEdzOWP2YBbJYuLF2ZB/ZddPMseM2sIevEeOLXznuwIDAQAB +AoGACny2cgNgHnomrq6ptwZmrO8KplZ+NGLRf8o9Lak6Bl5D+zDNcndGm5AELywL +xnuPXCDwZ+oS15ScrA6v0CZehgKoGhyqXl0nnRVlIYar30043DuaG6qiK302dHvo +NKP9CXGMyHq5i62M378KZgIX5MwT48KMnk7y4jFdw4/1BLECQQDp6LJToD4/p6H2 +cR9fIPLavsIN4rnVyHHGorF+6D/N/VXZaP3tfz45LCYXTpDtgyeT7CRaAOsmia9U +PGvJQ7vdAkEAzzg414RdOnl6Tl64vKhXmB79dnw1o/8lzCWEQOxQcJSfE6hLTJjO +yIRttBWdHvoZDmEYDnzx2V9stXszelOkdwJAaPFQiHpdUzvHS4TnOUvUCVYKRCaT +j5mVijPWBIT4k1d0m80iF9q9tPLcyBokpZrxN8l7abH9zlV5hwZwqWStNQJBAJ6c +tkWg94tuuIOMVXNyl7nE3tX2tUhWYKQmg1j+iYPi/vCY/SogPQeW9jHF6YJwhQVQ +pI1Sbfyo4hj3uKuFonkCQAfA0q38/kvG3Zsyx+1WxNjThcdJc6UZC9HRwRDZ2Sf/ +M00mX0l0NgsLkjRq0dCJtRoRKC29n3EpdG+sSdxVVo4= +-----END RSA PRIVATE KEY----- diff --git a/tests/data/owner.key b/tests/data/owner.key deleted file mode 100644 index 2400807..0000000 --- a/tests/data/owner.key +++ /dev/null @@ -1,12 +0,0 @@ ------BEGIN DSA PRIVATE KEY----- -MIIBvAIBAAKBgQDbmKBR97ryRqEBTHlujNwgiUJT60r+rKnfXRkJcJ5zcSwa6uio -GOt3tdFC7cmM6vVI4iFKMKm7YXhq7TwrS+cO6phXAp6tXBcwJOl8X3usrSQ8dX6p -eWNcCqmvs8TihekhjZ4Jcsb3+/JmV7dhusMdtgI1vtXxIQDq/oPJEYlGwQIVAMD9 -bct8Pk0EHi+VkVxPXSNTgHxDAoGAR4dVNge9Znprv07GrcDqPfgslMrxdBRx7F8E -A5HkY6mN3XH3ld/GKFPxGzmbRyEIHXxq88u5uaLJ6QcFLRy7p2SbsArebQTa6VPA -gBe5xFzieJ9hZ6DYljtSLG1nYZIfR3e/kwTPYSxpjqpakyMd05lBtHuBdl991VSf -FLKyd5sCgYEAm29WVw9zkRbv6CTFhPlLjJ71l/2GE9XFbdznJFRmPNBBWF2J452o -kRWywzeDMIIoi/z0wmNSr2B6P9wduxSxp8eIWQhKVQa4V4lJyqX/A2tE5SQtFULt -w3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgECFQC6wU/U -6uUSSSw8Apr+eJQlSFhA+Q== ------END DSA PRIVATE KEY----- diff --git a/tests/data/owner.key.pub b/tests/data/owner.key.pub deleted file mode 100644 index ddc7acb..0000000 --- a/tests/data/owner.key.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-dss AAAAB3NzaC1kc3MAAACBANuYoFH3uvJGoQFMeW6M3CCJQlPrSv6sqd9dGQlwnnNxLBrq6KgY63e10ULtyYzq9UjiIUowqbtheGrtPCtL5w7qmFcCnq1cFzAk6Xxfe6ytJDx1fql5Y1wKqa+zxOKF6SGNnglyxvf78mZXt2G6wx22AjW+1fEhAOr+g8kRiUbBAAAAFQDA/W3LfD5NBB4vlZFcT10jU4B8QwAAAIBHh1U2B71memu/TsatwOo9+CyUyvF0FHHsXwQDkeRjqY3dcfeV38YoU/EbOZtHIQgdfGrzy7m5osnpBwUtHLunZJuwCt5tBNrpU8CAF7nEXOJ4n2FnoNiWO1IsbWdhkh9Hd7+TBM9hLGmOqlqTIx3TmUG0e4F2X33VVJ8UsrJ3mwAAAIEAm29WVw9zkRbv6CTFhPlLjJ71l/2GE9XFbdznJFRmPNBBWF2J452okRWywzeDMIIoi/z0wmNSr2B6P9wduxSxp8eIWQhKVQa4V4lJyqX/A2tE5SQtFULtw3yiYOUaCjvB2s46ZM6/9K3r8o7FSKHDpYlqAbBKURNCot5zDAu6RgE= diff --git a/tests/integration/master_personal.py b/tests/integration/master_personal.py index f484836..6c1dfed 100755 --- a/tests/integration/master_personal.py +++ b/tests/integration/master_personal.py @@ -13,9 +13,9 @@ import rrdtool from __init__ import tests, src_root -from sugar_network.client import Connection, sugar_uid +from sugar_network.client import Connection, keyfile from sugar_network.toolkit.rrd import Rrd -from sugar_network.toolkit import coroutine +from sugar_network.toolkit import coroutine, http # /tmp might be on tmpfs wich returns 0 bytes for free mem all time @@ -45,12 +45,13 @@ class MasterPersonalTest(tests.Test): '--mounts-root=client/mnt', '--ipc-port=8102', '--stats-user', '--stats-user-step=1', '--stats-user-rras=RRA:AVERAGE:0.5:1:100', + '--keyfile=%s' % keyfile.value, ]) os.makedirs('client/mnt/disk/sugar-network') coroutine.sleep(2) ipc = Connection('http://127.0.0.1:8102') - if ipc.get(cmd='status')['route'] == 'offline': + if ipc.get(cmd='whoami')['route'] == 'offline': self.wait_for_events(ipc, event='inline', state='online').wait() Connection('http://127.0.0.1:8100').get(cmd='whoami') Connection('http://127.0.0.1:8101').get(cmd='whoami') @@ -61,9 +62,10 @@ class MasterPersonalTest(tests.Test): tests.Test.tearDown(self) def test_SyncMounts(self): - master = Connection('http://127.0.0.1:8100') - client = Connection('http://127.0.0.1:8102') - uid = sugar_uid() + auth = http.SugarAuth(keyfile.value) + master = Connection('http://127.0.0.1:8100', auth=auth) + client = Connection('http://127.0.0.1:8102', auth=auth) + uid = auth.login # Create shared files on master self.touch(('master/files/1/1', '1')) diff --git a/tests/integration/master_slave.py b/tests/integration/master_slave.py index 101c169..f521a2b 100755 --- a/tests/integration/master_slave.py +++ b/tests/integration/master_slave.py @@ -13,9 +13,9 @@ import rrdtool from __init__ import tests, src_root -from sugar_network.client import Connection +from sugar_network.client import Connection, keyfile from sugar_network.toolkit.rrd import Rrd -from sugar_network.toolkit import coroutine +from sugar_network.toolkit import coroutine, http # /tmp might be on tmpfs wich returns 0 bytes for free mem all time @@ -55,8 +55,8 @@ class MasterSlaveTest(tests.Test): def test_OnlineSync(self): ts = int(time.time()) - master = Connection('http://127.0.0.1:8100') - slave = Connection('http://127.0.0.1:8101') + master = Connection('http://127.0.0.1:8100', auth=http.SugarAuth(keyfile.value)) + slave = Connection('http://127.0.0.1:8101', auth=http.SugarAuth(keyfile.value)) # Initial data @@ -174,8 +174,8 @@ class MasterSlaveTest(tests.Test): self.assertEqual('file2', file('slave/files/file2').read()) def test_OfflineSync(self): - master = Connection('http://127.0.0.1:8100') - slave = Connection('http://127.0.0.1:8101') + master = Connection('http://127.0.0.1:8100', auth=http.SugarAuth(keyfile.value)) + slave = Connection('http://127.0.0.1:8101', auth=http.SugarAuth(keyfile.value)) # Create shared files on master self.touch(('master/files/1/1', '1')) diff --git a/tests/integration/node_client.py b/tests/integration/node_client.py index 3bb7999..a44ce77 100755 --- a/tests/integration/node_client.py +++ b/tests/integration/node_client.py @@ -129,18 +129,9 @@ class NodeClientTest(tests.Test): file('client/db/implementation/%s/%s/data.blob/activity/activity.info' % (impl[:2], impl)).read()) def test_UsecaseOOB(self): - privkey_path = '.sugar/default/owner.key' - pubkey_path = '.sugar/default/owner.key.pub' - os.unlink(privkey_path) - os.unlink(pubkey_path) - - self.cli(['PUT', '/context/context', '--no-dbus', '--anonymous', 'cmd=clone', '-jd', '1']) - self.cli(['PUT', '/context/context', '--no-dbus', '--anonymous', 'cmd=favorite', '-jd', 'true']) - + self.cli(['--quiet', 'PUT', '/context/context', 'cmd=clone', '-jd', 'true']) assert exists('client/db/implementation/im/implementation/data.blob/activity/activity.info') - self.assertEqual(['clone', 'favorite'], json.load(file('client/db/context/co/context/layer'))['value']) - assert not exists(privkey_path) - assert not exists(pubkey_path) + self.assertEqual(['clone'], json.load(file('client/db/context/co/context/layer'))['value']) def cli(self, cmd, stdin=None): cmd = ['sugar-network', '--local-root=client', '--ipc-port=5101', '--api-url=http://127.0.0.1:8100', '-DDD'] + cmd @@ -154,7 +145,7 @@ class NodeClientTest(tests.Test): ]) coroutine.sleep(2) ipc = Connection('http://127.0.0.1:5101') - if ipc.get(cmd='status')['route'] == 'offline': + if ipc.get(cmd='whoami')['route'] == 'offline': self.wait_for_events(ipc, event='inline', state='online').wait() result = toolkit.assert_call(cmd, stdin=json.dumps(stdin)) diff --git a/tests/integration/node_packages.py b/tests/integration/node_packages.py index a45b788..f4bae67 100755 --- a/tests/integration/node_packages.py +++ b/tests/integration/node_packages.py @@ -15,11 +15,11 @@ import rrdtool from __init__ import tests, src_root from sugar_network import db, client -from sugar_network.client import Connection, IPCConnection +from sugar_network.client import IPCConnection, Connection, keyfile from sugar_network.node.obs import obs_url from sugar_network.toolkit.router import Router, route, fallbackroute from sugar_network.toolkit.rrd import Rrd -from sugar_network.toolkit import coroutine +from sugar_network.toolkit import coroutine, http # /tmp might be on tmpfs wich returns 0 bytes for free mem all time @@ -75,7 +75,7 @@ class NodePackagesSlaveTest(tests.Test): '--obs-url=http://127.0.0.1:1999', ])) coroutine.sleep(3) - conn = Connection('http://127.0.0.1:8100') + conn = Connection('http://127.0.0.1:8100', auth=http.SugarAuth(keyfile.value)) conn.post(['/context'], { 'guid': 'package', @@ -106,7 +106,7 @@ class NodePackagesSlaveTest(tests.Test): client.ipc_port.value = 8200 ipc = IPCConnection() coroutine.sleep(2) - if ipc.get(cmd='status')['route'] == 'offline': + if ipc.get(cmd='whoami')['route'] == 'offline': self.wait_for_events(ipc, event='inline', state='online').wait() self.assertEqual( '{"arch": [{"path": "rpm", "name": "rpm"}]}', @@ -144,7 +144,7 @@ class NodePackagesSlaveTest(tests.Test): client.ipc_port.value = 8200 ipc = IPCConnection() coroutine.sleep(2) - if ipc.get(cmd='status')['route'] == 'offline': + if ipc.get(cmd='whoami')['route'] == 'offline': self.wait_for_events(ipc, event='inline', state='online').wait() self.assertEqual( '{"arch": [{"path": "rpm", "name": "rpm"}]}', @@ -165,7 +165,7 @@ class NodePackagesSlaveTest(tests.Test): conn = Connection('http://127.0.0.1:8102') client.ipc_port.value = 8202 ipc = IPCConnection() - if ipc.get(cmd='status')['route'] == 'offline': + if ipc.get(cmd='whoami')['route'] == 'offline': self.wait_for_events(ipc, event='inline', state='online').wait() pid = self.popen('V=1 %s sync/sugar-network-sync http://127.0.0.1:8100' % join(src_root, 'sugar-network-sync'), shell=True) diff --git a/tests/units/client/offline_routes.py b/tests/units/client/offline_routes.py index 5725cf2..673f6b2 100755 --- a/tests/units/client/offline_routes.py +++ b/tests/units/client/offline_routes.py @@ -27,7 +27,7 @@ class OfflineRoutes(tests.Test): ipc = self.start_offline_client() self.assertEqual( - {'guid': tests.UID, 'roles': []}, + {'guid': tests.UID, 'roles': [], 'route': 'offline'}, ipc.get(cmd='whoami')) def test_Events(self): diff --git a/tests/units/client/online_routes.py b/tests/units/client/online_routes.py index 6ad7484..20bc2a1 100755 --- a/tests/units/client/online_routes.py +++ b/tests/units/client/online_routes.py @@ -42,7 +42,7 @@ class OnlineRoutes(tests.Test): ipc = IPCConnection() self.assertEqual( - {'guid': tests.UID, 'roles': []}, + {'guid': tests.UID, 'roles': [], 'route': 'proxy'}, ipc.get(cmd='whoami')) def test_Events(self): @@ -70,6 +70,7 @@ class OnlineRoutes(tests.Test): coroutine.sleep(.1) self.assertEqual([ + {'guid': tests.UID, 'resource': 'user', 'event': 'create'}, {'guid': guid, 'resource': 'context', 'event': 'create'}, {'guid': guid, 'resource': 'context', 'event': 'update'}, {'guid': guid, 'event': 'delete', 'resource': 'context'}, @@ -1394,7 +1395,7 @@ Can't find all required implementations: def __init__(self, *args): pass - @route('GET', cmd='info', mime_type='application/json') + @route('GET', cmd='status', mime_type='application/json') def info(self): return {'resources': {}} diff --git a/tests/units/client/routes.py b/tests/units/client/routes.py index 53b1924..bfd2330 100755 --- a/tests/units/client/routes.py +++ b/tests/units/client/routes.py @@ -282,7 +282,7 @@ class RoutesTest(tests.Test): self.assertEqual('title', call(cp, Request(method='GET', path=['context', guid, 'title']))) def test_I18nQuery(self): - client.accept_language.value = 'foo' + os.environ['LANGUAGE'] = 'foo' self.start_online_client() ipc = IPCConnection() @@ -419,69 +419,30 @@ class RoutesTest(tests.Test): 'description': '', }) - client.accept_language.value = ['es', 'ru', 'en'] - self.assertEqual('3', ipc.get(['context', guid1, 'title'])) - self.assertEqual('2', ipc.get(['context', guid2, 'title'])) - self.assertEqual('1', ipc.get(['context', guid3, 'title'])) - - client.accept_language.value = ['ru', 'en'] - self.assertEqual('2', ipc.get(['context', guid1, 'title'])) - self.assertEqual('2', ipc.get(['context', guid2, 'title'])) - self.assertEqual('1', ipc.get(['context', guid3, 'title'])) - - client.accept_language.value = ['en'] - self.assertEqual('1', ipc.get(['context', guid1, 'title'])) - self.assertEqual('1', ipc.get(['context', guid2, 'title'])) - self.assertEqual('1', ipc.get(['context', guid3, 'title'])) - - client.accept_language.value = ['foo'] - self.assertEqual('1', ipc.get(['context', guid1, 'title'])) - self.assertEqual('1', ipc.get(['context', guid2, 'title'])) - self.assertEqual('1', ipc.get(['context', guid3, 'title'])) - - def test_DefaultLanguagesFallbackInRequests(self): - self.start_online_client() - ipc = IPCConnection() - - guid1 = self.node_volume['context'].create({ - 'type': 'activity', - 'title': {'en': '1', 'ru': '2', 'es': '3'}, - 'summary': '', - 'description': '', - }) - guid2 = self.node_volume['context'].create({ - 'type': 'activity', - 'title': {'en': '1', 'ru': '2'}, - 'summary': '', - 'description': '', - }) - guid3 = self.node_volume['context'].create({ - 'type': 'activity', - 'title': {'en': '1'}, - 'summary': '', - 'description': '', - }) - toolkit._default_langs = None os.environ['LANGUAGE'] = 'es:ru:en' + ipc = IPCConnection() self.assertEqual('3', ipc.get(['context', guid1, 'title'])) self.assertEqual('2', ipc.get(['context', guid2, 'title'])) self.assertEqual('1', ipc.get(['context', guid3, 'title'])) toolkit._default_langs = None os.environ['LANGUAGE'] = 'ru:en' + ipc = IPCConnection() self.assertEqual('2', ipc.get(['context', guid1, 'title'])) self.assertEqual('2', ipc.get(['context', guid2, 'title'])) self.assertEqual('1', ipc.get(['context', guid3, 'title'])) toolkit._default_langs = None os.environ['LANGUAGE'] = 'en' + ipc = IPCConnection() self.assertEqual('1', ipc.get(['context', guid1, 'title'])) self.assertEqual('1', ipc.get(['context', guid2, 'title'])) self.assertEqual('1', ipc.get(['context', guid3, 'title'])) toolkit._default_langs = None os.environ['LANGUAGE'] = 'foo' + ipc = IPCConnection() self.assertEqual('1', ipc.get(['context', guid1, 'title'])) self.assertEqual('1', ipc.get(['context', guid2, 'title'])) self.assertEqual('1', ipc.get(['context', guid3, 'title'])) diff --git a/tests/units/client/server_routes.py b/tests/units/client/server_routes.py index 6b36061..c477ff2 100755 --- a/tests/units/client/server_routes.py +++ b/tests/units/client/server_routes.py @@ -22,7 +22,7 @@ class ServerRoutesTest(tests.Test): ipc = IPCConnection() self.assertEqual( - {'guid': tests.UID, 'roles': []}, + {'guid': tests.UID, 'roles': [], 'route': 'proxy'}, ipc.get(cmd='whoami')) def test_Events(self): @@ -119,17 +119,6 @@ class ServerRoutesTest(tests.Test): events) del events[:] - - - - - - - - - - - def test_BLOBs(self): self.start_node() ipc = IPCConnection() diff --git a/tests/units/model/comment.py b/tests/units/model/comment.py index 4b97723..048a6a4 100755 --- a/tests/units/model/comment.py +++ b/tests/units/model/comment.py @@ -3,7 +3,7 @@ from __init__ import tests -from sugar_network.client import Connection +from sugar_network.client import Connection, keyfile from sugar_network.model.user import User from sugar_network.model.context import Context from sugar_network.model.review import Review @@ -18,7 +18,7 @@ class CommentTest(tests.Test): def test_SetContext(self): volume = self.start_master([User, Context, Review, Feedback, Solution, Comment, Implementation]) - client = Connection() + client = Connection(auth=http.SugarAuth(keyfile.value)) self.assertRaises(http.NotFound, client.post, ['comment'], {'message': '', 'review': 'absent'}) self.assertRaises(http.NotFound, client.post, ['comment'], {'message': '', 'feedback': 'absent'}) diff --git a/tests/units/model/review.py b/tests/units/model/review.py index 5cde1df..52ee54a 100755 --- a/tests/units/model/review.py +++ b/tests/units/model/review.py @@ -3,19 +3,20 @@ from __init__ import tests -from sugar_network.client import Connection +from sugar_network.client import Connection, keyfile from sugar_network.model.user import User from sugar_network.model.context import Context from sugar_network.model.review import Review from sugar_network.model.artifact import Artifact from sugar_network.model.implementation import Implementation +from sugar_network.toolkit import http class ReviewTest(tests.Test): def test_SetContext(self): volume = self.start_master([User, Context, Review, Artifact, Implementation]) - client = Connection() + client = Connection(auth=http.SugarAuth(keyfile.value)) context = client.post(['context'], { 'type': 'package', diff --git a/tests/units/model/solution.py b/tests/units/model/solution.py index 908653d..f550d97 100755 --- a/tests/units/model/solution.py +++ b/tests/units/model/solution.py @@ -3,19 +3,20 @@ from __init__ import tests -from sugar_network.client import Connection +from sugar_network.client import Connection, keyfile from sugar_network.model.user import User from sugar_network.model.context import Context from sugar_network.model.feedback import Feedback from sugar_network.model.solution import Solution from sugar_network.model.implementation import Implementation +from sugar_network.toolkit import http class SolutionTest(tests.Test): def test_SetContext(self): volume = self.start_master([User, Context, Feedback, Solution, Implementation]) - client = Connection() + client = Connection(auth=http.SugarAuth(keyfile.value)) context = client.post(['context'], { 'type': 'package', diff --git a/tests/units/node/node.py b/tests/units/node/node.py index c39d185..4f090fc 100755 --- a/tests/units/node/node.py +++ b/tests/units/node/node.py @@ -6,14 +6,16 @@ import os import time import json import base64 +import hashlib +from M2Crypto import RSA from email.utils import formatdate, parsedate from cStringIO import StringIO -from os.path import exists +from os.path import exists, join from __init__ import tests from sugar_network import db, node, model, client -from sugar_network.client import Connection +from sugar_network.client import Connection, keyfile from sugar_network.toolkit import http, coroutine from sugar_network.toolkit.rrd import Rrd from sugar_network.node import stats_user, stats_node, obs @@ -28,6 +30,7 @@ from sugar_network.model.artifact import Artifact from sugar_network.model.solution import Solution from sugar_network.model.user import User from sugar_network.toolkit.router import Router, Request, Response, fallbackroute, Blob, ACL, route +from sugar_network.toolkit import http class NodeTest(tests.Test): @@ -45,8 +48,6 @@ class NodeTest(tests.Test): call(cp, method='POST', document='user', principal=tests.UID, content={ 'name': 'user', 'color': '', - 'machine_sn': '', - 'machine_uuid': '', 'pubkey': tests.PUBKEY, }) @@ -137,7 +138,7 @@ class NodeTest(tests.Test): def test_HandleDeletes(self): volume = db.Volume('db', model.RESOURCES) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) cp = NodeRoutes('guid', volume) guid = call(cp, method='POST', document='context', principal=tests.UID, content={ @@ -172,7 +173,7 @@ class NodeTest(tests.Test): def test_SimulateDeleteEvents(self): volume = db.Volume('db', model.RESOURCES) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) cp = NodeRoutes('guid', volume) guid = call(cp, method='POST', document='context', principal=tests.UID, content={ @@ -196,19 +197,17 @@ class NodeTest(tests.Test): def test_RegisterUser(self): cp = NodeRoutes('guid', db.Volume('db', [User])) - guid = call(cp, method='POST', document='user', principal='fake', content={ + guid = call(cp, method='POST', document='user', principal=tests.UID2, content={ 'name': 'user', 'color': '', - 'machine_sn': '', - 'machine_uuid': '', 'pubkey': tests.PUBKEY, }) - assert guid == tests.UID + assert guid is None self.assertEqual('user', call(cp, method='GET', document='user', guid=tests.UID, prop='name')) def test_UnauthorizedCommands(self): volume = db.Volume('db', model.RESOURCES) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) class Routes(NodeRoutes): @@ -246,30 +245,28 @@ class NodeTest(tests.Test): pass volume = db.Volume('db', [User, Document]) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) cp = Routes('guid', volume) guid = call(cp, method='POST', document='document', principal=tests.UID, content={}) self.assertRaises(http.Forbidden, call, cp, method='GET', cmd='probe1', document='document', guid=guid) - self.assertRaises(http.Unauthorized, call, cp, method='GET', cmd='probe1', document='document', guid=guid, principal='fake') + self.assertRaises(http.Forbidden, call, cp, method='GET', cmd='probe1', document='document', guid=guid, principal=tests.UID2) call(cp, method='GET', cmd='probe1', document='document', guid=guid, principal=tests.UID) call(cp, method='GET', cmd='probe2', document='document', guid=guid) def test_ForbiddenCommandsForUserResource(self): cp = NodeRoutes('guid', db.Volume('db', [User])) - call(cp, method='POST', document='user', principal='fake', content={ + call(cp, method='POST', document='user', principal=tests.UID2, content={ 'name': 'user1', 'color': '', - 'machine_sn': '', - 'machine_uuid': '', 'pubkey': tests.PUBKEY, }) self.assertEqual('user1', call(cp, method='GET', document='user', guid=tests.UID, prop='name')) self.assertRaises(http.Unauthorized, call, cp, method='PUT', document='user', guid=tests.UID, content={'name': 'user2'}) - self.assertRaises(http.Unauthorized, call, cp, method='PUT', document='user', guid=tests.UID, principal='fake', content={'name': 'user2'}) + self.assertRaises(http.Forbidden, call, cp, method='PUT', document='user', guid=tests.UID, principal=tests.UID2, content={'name': 'user2'}) call(cp, method='PUT', document='user', guid=tests.UID, principal=tests.UID, content={'name': 'user2'}) self.assertEqual('user2', call(cp, method='GET', document='user', guid=tests.UID, prop='name')) @@ -286,8 +283,8 @@ class NodeTest(tests.Test): return 'ok' volume = db.Volume('db', [User]) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) - volume['user'].create({'guid': tests.UID2, 'name': 'test', 'color': '', 'pubkey': tests.PUBKEY2}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) + volume['user'].create({'guid': tests.UID2, 'name': 'test', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY2)}}) cp = Routes('guid', volume) self.assertRaises(http.Forbidden, call, cp, method='PROBE') @@ -303,8 +300,8 @@ class NodeTest(tests.Test): return value volume = db.Volume('db', [User, Document]) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) - volume['user'].create({'guid': tests.UID2, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY2}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) + volume['user'].create({'guid': tests.UID2, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY2)}}) cp = NodeRoutes('guid', volume) guid = call(cp, method='POST', document='document', principal=tests.UID, content={'prop': '1'}) @@ -324,8 +321,8 @@ class NodeTest(tests.Test): return value volume = db.Volume('db', [User, Document]) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) - volume['user'].create({'guid': tests.UID2, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY2}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) + volume['user'].create({'guid': tests.UID2, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY2)}}) cp = NodeRoutes('guid', volume) guid = call(cp, method='POST', document='document', principal=tests.UID, content={'prop': '1'}) @@ -345,7 +342,7 @@ class NodeTest(tests.Test): pass volume = db.Volume('db', [User]) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) cp = Routes('guid', volume) self.assertRaises(http.Forbidden, call, cp, 'PROBE', principal=tests.UID) @@ -383,7 +380,7 @@ class NodeTest(tests.Test): def test_SetUser(self): volume = db.Volume('db', model.RESOURCES) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) cp = NodeRoutes('guid', volume) guid = call(cp, method='POST', document='context', principal=tests.UID, content={ @@ -398,7 +395,7 @@ class NodeTest(tests.Test): def test_find_MaxLimit(self): volume = db.Volume('db', model.RESOURCES) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) cp = NodeRoutes('guid', volume) call(cp, method='POST', document='context', principal=tests.UID, content={ @@ -429,7 +426,7 @@ class NodeTest(tests.Test): def test_DeletedDocuments(self): volume = db.Volume('db', model.RESOURCES) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) cp = NodeRoutes('guid', volume) guid = call(cp, method='POST', document='context', principal=tests.UID, content={ @@ -450,7 +447,7 @@ class NodeTest(tests.Test): def test_CreateGUID(self): # TODO Temporal security hole, see TODO volume = db.Volume('db', model.RESOURCES) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) cp = NodeRoutes('guid', volume) call(cp, method='POST', document='context', principal=tests.UID, content={ 'guid': 'foo', @@ -465,7 +462,7 @@ class NodeTest(tests.Test): def test_CreateMalformedGUID(self): volume = db.Volume('db', model.RESOURCES) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) cp = MasterRoutes('guid', volume) self.assertRaises(RuntimeError, call, cp, method='POST', document='context', principal=tests.UID, content={ @@ -478,7 +475,7 @@ class NodeTest(tests.Test): def test_FailOnExistedGUID(self): volume = db.Volume('db', model.RESOURCES) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': tests.PUBKEY}) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'color': '', 'pubkey': {'blob': StringIO(tests.PUBKEY)}}) cp = MasterRoutes('guid', volume) guid = call(cp, method='POST', document='context', principal=tests.UID, content={ @@ -500,7 +497,7 @@ class NodeTest(tests.Test): node.files_root.value = '.' self.touch(('packages/repo/arch/package', 'file')) volume = self.start_master() - client = Connection() + client = Connection(auth=http.SugarAuth(keyfile.value)) self.assertEqual(['repo'], client.get(['packages'])) self.assertEqual(['arch'], client.get(['packages', 'repo'])) @@ -516,7 +513,7 @@ class NodeTest(tests.Test): ('packages/repo/2.2', '', 2), ) volume = self.start_master() - ipc = Connection() + ipc = Connection(auth=http.SugarAuth(keyfile.value)) self.assertEqual( sorted(['1', '2']), @@ -548,7 +545,7 @@ class NodeTest(tests.Test): def test_Clone(self): volume = self.start_master() - client = Connection() + client = Connection(auth=http.SugarAuth(keyfile.value)) context = client.post(['context'], { 'type': 'activity', @@ -666,7 +663,7 @@ class NodeTest(tests.Test): def test_release(self): volume = self.start_master() - conn = Connection() + conn = Connection(auth=http.SugarAuth(keyfile.value)) conn.post(['context'], { 'guid': 'bundle_id', @@ -697,7 +694,7 @@ class NodeTest(tests.Test): self.assertEqual('developer', impl['stability']) assert impl['ctime'] > 0 assert impl['mtime'] > 0 - self.assertEqual({tests.UID: {'role': 3, 'name': 'test', 'order': 0}}, impl['author']) + self.assertEqual({tests.UID: {'role': 3, 'name': 'f470db873b6a35903aca1f2492188e1c4b9ffc42', 'order': 0}}, impl['author']) data = impl.meta('data') self.assertEqual({ @@ -778,7 +775,7 @@ class NodeTest(tests.Test): def test_release_UpdateContext(self): volume = self.start_master() - conn = Connection() + conn = Connection(auth=http.SugarAuth(keyfile.value)) conn.post(['context'], { 'guid': 'org.laptop.ImageViewerActivity', @@ -847,7 +844,7 @@ class NodeTest(tests.Test): def test_release_CreateContext(self): volume = self.start_master() - conn = Connection() + conn = Connection(auth=http.SugarAuth(keyfile.value)) bundle = self.zips( ('ImageViewer.activity/activity/activity.info', '\n'.join([ @@ -876,7 +873,7 @@ class NodeTest(tests.Test): self.assertEqual(['image/bmp', 'image/gif'], context['mime_types']) assert context['ctime'] > 0 assert context['mtime'] > 0 - self.assertEqual({tests.UID: {'role': 3, 'name': 'test', 'order': 0}}, context['author']) + self.assertEqual({tests.UID: {'role': 3, 'name': 'f470db873b6a35903aca1f2492188e1c4b9ffc42', 'order': 0}}, context['author']) def test_release_AuthorsOnly(self): volume = self.start_master() @@ -893,42 +890,42 @@ class NodeTest(tests.Test): ('ImageViewer.activity/activity/activity-imageviewer.svg', ''), ) - self.override(client, 'sugar_uid', lambda: tests.UID) - conn = Connection() + conn = Connection(auth=http.SugarAuth(join(tests.root, 'data', tests.UID))) impl1 = json.load(conn.request('POST', ['implementation'], bundle, params={'cmd': 'submit', 'initial': 1}).raw) impl2 = json.load(conn.request('POST', ['implementation'], bundle, params={'cmd': 'submit'}).raw) self.assertEqual(['deleted'], volume['implementation'].get(impl1)['layer']) self.assertEqual([], volume['implementation'].get(impl2)['layer']) - self.override(client, 'sugar_uid', lambda: tests.UID2) - self.override(client, 'sugar_profile', lambda: { - 'name': 'test', - 'color': '#000000,#000000', - 'machine_sn': '', - 'machine_uuid': '', - 'pubkey': tests.PUBKEY2, - }) - conn = Connection() + conn = Connection(auth=http.SugarAuth(join(tests.root, 'data', tests.UID2))) conn.get(cmd='whoami') self.assertRaises(http.Forbidden, conn.request, 'POST', ['implementation'], bundle, params={'cmd': 'submit'}) self.assertEqual(['deleted'], volume['implementation'].get(impl1)['layer']) self.assertEqual([], volume['implementation'].get(impl2)['layer']) -def call(routes, method, document=None, guid=None, prop=None, principal=None, cmd=None, content=None, **kwargs): - path = [] +def call(routes, method, document=None, guid=None, prop=None, principal=None, content=None, **kwargs): + path = [''] if document: path.append(document) if guid: path.append(guid) if prop: path.append(prop) - request = Request(method=method, path=path, cmd=cmd, content=content) - request.update(kwargs) - request.environ['HTTP_HOST'] = '127.0.0.1' + env = {'REQUEST_METHOD': method, + 'PATH_INFO': '/'.join(path), + 'HTTP_HOST': '127.0.0.1', + } if principal: - request.environ['HTTP_X_SN_LOGIN'] = principal - request.principal = principal + key = RSA.load_key(join(tests.root, 'data', principal)) + nonce = int(time.time()) + 100 + data = hashlib.sha1('%s:%s' % (principal, nonce)).digest() + signature = key.sign(data).encode('hex') + env['HTTP_AUTHORIZATION'] = 'Sugar username="%s",nonce="%s",signature="%s"' % (principal, nonce, signature) + request = Request(env) + request.update(kwargs) + request.cmd = kwargs.get('cmd') + request.content = content + request.principal = principal router = Router(routes) return router.call(request, Response()) diff --git a/tests/units/node/sync_offline.py b/tests/units/node/sync_offline.py index 0353e58..1c4ffd5 100755 --- a/tests/units/node/sync_offline.py +++ b/tests/units/node/sync_offline.py @@ -48,7 +48,6 @@ class SyncOfflineTest(tests.Test): pass volume = Volume('node', [Document]) - toolkit.ensure_key('node/key') cp = SlaveRoutes('node/key', volume) node.sync_layers.value = None @@ -62,7 +61,6 @@ class SyncOfflineTest(tests.Test): pass volume = Volume('node', [Document]) - toolkit.ensure_key('node/key') cp = SlaveRoutes('node/key', volume) stats_user.stats_user.value = True @@ -114,7 +112,6 @@ class SyncOfflineTest(tests.Test): return value volume = Volume('node', [Document]) - toolkit.ensure_key('node/key') cp = SlaveRoutes('node/key', volume) stats_user.stats_user.value = True @@ -205,7 +202,6 @@ class SyncOfflineTest(tests.Test): pass volume = Volume('node', [Document]) - toolkit.ensure_key('node/key') cp = SlaveRoutes('node/key', volume) stats_user.stats_user.value = True files_root.value = 'files' diff --git a/tests/units/node/sync_online.py b/tests/units/node/sync_online.py index 6a88875..400ba4b 100755 --- a/tests/units/node/sync_online.py +++ b/tests/units/node/sync_online.py @@ -8,7 +8,7 @@ from os.path import exists from __init__ import tests from sugar_network import db, toolkit -from sugar_network.client import Connection, api_url +from sugar_network.client import Connection, api_url, keyfile from sugar_network.node import sync, stats_user, files_root from sugar_network.node.master import MasterRoutes from sugar_network.node.slave import SlaveRoutes @@ -16,7 +16,7 @@ from sugar_network.db.volume import Volume from sugar_network.model.user import User from sugar_network.model.feedback import Feedback from sugar_network.toolkit.router import Router -from sugar_network.toolkit import coroutine +from sugar_network.toolkit import coroutine, http class SyncOnlineTest(tests.Test): @@ -48,7 +48,6 @@ class SyncOnlineTest(tests.Test): files_root.value = 'slave/files' self.slave_volume = Volume('slave', [User, Document]) - toolkit.ensure_key('slave/node') self.slave_server = coroutine.WSGIServer(('127.0.0.1', 9001), Router(SlaveRoutes('slave/node', self.slave_volume))) coroutine.spawn(self.slave_server.serve_forever) coroutine.dispatch() @@ -59,11 +58,12 @@ class SyncOnlineTest(tests.Test): tests.Test.tearDown(self) def test_Push(self): - client = Connection('http://127.0.0.1:9001') + client = Connection('http://127.0.0.1:9001', auth=http.SugarAuth(keyfile.value)) # Sync users + client.get(cmd='logon') client.post(cmd='online-sync') - self.assertEqual([[3, None]], json.load(file('slave/pull.sequence'))) + self.assertEqual([[4, None]], json.load(file('slave/pull.sequence'))) self.assertEqual([[2, None]], json.load(file('slave/push.sequence'))) guid1 = client.post(['document'], {'context': '', 'content': '1', 'title': '', 'type': 'idea'}) @@ -75,7 +75,7 @@ class SyncOnlineTest(tests.Test): {'guid': guid2, 'content': {'en-us': '2'}}, ], [i.properties(['guid', 'content']) for i in self.master_volume['document'].find()[0]]) - self.assertEqual([[5, None]], json.load(file('slave/pull.sequence'))) + self.assertEqual([[6, None]], json.load(file('slave/pull.sequence'))) self.assertEqual([[4, None]], json.load(file('slave/push.sequence'))) guid3 = client.post(['document'], {'context': '', 'content': '3', 'title': '', 'type': 'idea'}) @@ -86,7 +86,7 @@ class SyncOnlineTest(tests.Test): {'guid': guid3, 'content': {'en-us': '3'}}, ], [i.properties(['guid', 'content']) for i in self.master_volume['document'].find()[0]]) - self.assertEqual([[6, None]], json.load(file('slave/pull.sequence'))) + self.assertEqual([[7, None]], json.load(file('slave/pull.sequence'))) self.assertEqual([[5, None]], json.load(file('slave/push.sequence'))) coroutine.sleep(1) @@ -95,7 +95,7 @@ class SyncOnlineTest(tests.Test): self.assertEqual( {'guid': guid2, 'content': {'en-us': '22'}}, self.master_volume['document'].get(guid2).properties(['guid', 'content'])) - self.assertEqual([[7, None]], json.load(file('slave/pull.sequence'))) + self.assertEqual([[8, None]], json.load(file('slave/pull.sequence'))) self.assertEqual([[6, None]], json.load(file('slave/push.sequence'))) coroutine.sleep(1) @@ -107,7 +107,7 @@ class SyncOnlineTest(tests.Test): {'guid': guid3, 'content': {'en-us': '3'}, 'layer': []}, ], [i.properties(['guid', 'content', 'layer']) for i in self.master_volume['document'].find()[0]]) - self.assertEqual([[8, None]], json.load(file('slave/pull.sequence'))) + self.assertEqual([[9, None]], json.load(file('slave/pull.sequence'))) self.assertEqual([[7, None]], json.load(file('slave/push.sequence'))) coroutine.sleep(1) @@ -124,23 +124,24 @@ class SyncOnlineTest(tests.Test): {'guid': guid4, 'content': {'en-us': 'd'}, 'layer': []}, ], [i.properties(['guid', 'content', 'layer']) for i in self.master_volume['document'].find()[0]]) - self.assertEqual([[12, None]], json.load(file('slave/pull.sequence'))) + self.assertEqual([[13, None]], json.load(file('slave/pull.sequence'))) self.assertEqual([[12, None]], json.load(file('slave/push.sequence'))) def test_PushStats(self): stats_user.stats_user.value = True - client = Connection('http://127.0.0.1:9001') + client = Connection('http://127.0.0.1:9001', auth=http.SugarAuth(keyfile.value)) client.post(cmd='online-sync') self.assertEqual(['ok'], self.stats_commit) self.assertEqual([{'stats': 'probe'}], self.stats_merge) def test_Pull(self): - client = Connection('http://127.0.0.1:9000') - slave_client = Connection('http://127.0.0.1:9001') + client = Connection('http://127.0.0.1:9000', auth=http.SugarAuth(keyfile.value)) + slave_client = Connection('http://127.0.0.1:9001', auth=http.SugarAuth(keyfile.value)) # Sync users + slave_client.get(cmd='logon') slave_client.post(cmd='online-sync') - self.assertEqual([[3, None]], json.load(file('slave/pull.sequence'))) + self.assertEqual([[4, None]], json.load(file('slave/pull.sequence'))) self.assertEqual([[2, None]], json.load(file('slave/push.sequence'))) guid1 = client.post(['document'], {'context': '', 'content': '1', 'title': '', 'type': 'idea'}) @@ -152,7 +153,7 @@ class SyncOnlineTest(tests.Test): {'guid': guid2, 'content': {'en-us': '2'}}, ], [i.properties(['guid', 'content']) for i in self.slave_volume['document'].find()[0]]) - self.assertEqual([[5, None]], json.load(file('slave/pull.sequence'))) + self.assertEqual([[6, None]], json.load(file('slave/pull.sequence'))) self.assertEqual([[2, None]], json.load(file('slave/push.sequence'))) guid3 = client.post(['document'], {'context': '', 'content': '3', 'title': '', 'type': 'idea'}) @@ -163,7 +164,7 @@ class SyncOnlineTest(tests.Test): {'guid': guid3, 'content': {'en-us': '3'}}, ], [i.properties(['guid', 'content']) for i in self.slave_volume['document'].find()[0]]) - self.assertEqual([[6, None]], json.load(file('slave/pull.sequence'))) + self.assertEqual([[7, None]], json.load(file('slave/pull.sequence'))) self.assertEqual([[2, None]], json.load(file('slave/push.sequence'))) coroutine.sleep(1) @@ -172,7 +173,7 @@ class SyncOnlineTest(tests.Test): self.assertEqual( {'guid': guid2, 'content': {'en-us': '22'}}, self.slave_volume['document'].get(guid2).properties(['guid', 'content'])) - self.assertEqual([[7, None]], json.load(file('slave/pull.sequence'))) + self.assertEqual([[8, None]], json.load(file('slave/pull.sequence'))) self.assertEqual([[2, None]], json.load(file('slave/push.sequence'))) coroutine.sleep(1) @@ -184,7 +185,7 @@ class SyncOnlineTest(tests.Test): {'guid': guid3, 'content': {'en-us': '3'}, 'layer': []}, ], [i.properties(['guid', 'content', 'layer']) for i in self.slave_volume['document'].find()[0]]) - self.assertEqual([[8, None]], json.load(file('slave/pull.sequence'))) + self.assertEqual([[9, None]], json.load(file('slave/pull.sequence'))) self.assertEqual([[2, None]], json.load(file('slave/push.sequence'))) coroutine.sleep(1) @@ -201,7 +202,7 @@ class SyncOnlineTest(tests.Test): {'guid': guid4, 'content': {'en-us': 'd'}, 'layer': []}, ], [i.properties(['guid', 'content', 'layer']) for i in self.slave_volume['document'].find()[0]]) - self.assertEqual([[13, None]], json.load(file('slave/pull.sequence'))) + self.assertEqual([[14, None]], json.load(file('slave/pull.sequence'))) self.assertEqual([[2, None]], json.load(file('slave/push.sequence'))) def test_PullFiles(self): @@ -210,30 +211,31 @@ class SyncOnlineTest(tests.Test): self.touch(('master/files/3/3/3', 'ccc', 3)) os.utime('master/files', (1, 1)) - client = Connection('http://127.0.0.1:9001') + client = Connection('http://127.0.0.1:9001', auth=http.SugarAuth(keyfile.value)) client.post(cmd='online-sync') files, stamp = json.load(file('master/files.index')) self.assertEqual(1, stamp) self.assertEqual(sorted([ - [3, '1', 1], - [4, '2/2', 2], - [5, '3/3/3', 3], + [2, '1', 1], + [3, '2/2', 2], + [4, '3/3/3', 3], ]), sorted(files)) - self.assertEqual([[6, None]], json.load(file('slave/files.sequence'))) + self.assertEqual([[5, None]], json.load(file('slave/files.sequence'))) self.assertEqual('a', file('slave/files/1').read()) self.assertEqual('bb', file('slave/files/2/2').read()) self.assertEqual('ccc', file('slave/files/3/3/3').read()) def test_PullFromPreviouslyMergedRecord(self): - master = Connection('http://127.0.0.1:9000') - slave = Connection('http://127.0.0.1:9001') + master = Connection('http://127.0.0.1:9000', auth=http.SugarAuth(keyfile.value)) + slave = Connection('http://127.0.0.1:9001', auth=http.SugarAuth(keyfile.value)) # Sync users + slave.get(cmd='logon') slave.post(cmd='online-sync') - self.assertEqual([[3, None]], json.load(file('slave/pull.sequence'))) + self.assertEqual([[4, None]], json.load(file('slave/pull.sequence'))) self.assertEqual([[2, None]], json.load(file('slave/push.sequence'))) guid = slave.post(['document'], {'context': '', 'content': '1', 'title': '1', 'type': 'idea'}) diff --git a/tests/units/toolkit/router.py b/tests/units/toolkit/router.py index 131e587..e229f2d 100755 --- a/tests/units/toolkit/router.py +++ b/tests/units/toolkit/router.py @@ -505,7 +505,7 @@ class RouterTest(tests.Test): return request['probe'] @preroute - def preroute(self, op, request): + def preroute(self, op, request, response): request['probe'] = 'request' router = Router(Routes()) -- cgit v0.9.1