Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@sugarlabs.org>2013-10-21 08:06:00 (GMT)
committer Aleksey Lim <alsroot@sugarlabs.org>2013-10-21 08:06:00 (GMT)
commit8223b2b538540aa227a30cc18434571455af5bc3 (patch)
tree1c5c275ff8c6471687335d50aee6666dd2f67412
parent58471a0a713dd3ef77276a3d0df23c44a5ddeb0d (diff)
Switch to RSA auth keys; keep private key per user, not per sugar profile
-rw-r--r--TODO5
-rwxr-xr-xsugar-network100
-rwxr-xr-xsugar-network-client10
-rwxr-xr-xsugar-network-node2
-rw-r--r--sugar_network/client/__init__.py169
-rw-r--r--sugar_network/client/routes.py81
-rw-r--r--sugar_network/client/solver.py10
-rw-r--r--sugar_network/db/directory.py5
-rw-r--r--sugar_network/db/routes.py7
-rw-r--r--sugar_network/model/routes.py38
-rw-r--r--sugar_network/model/user.py16
-rw-r--r--sugar_network/node/__init__.py6
-rw-r--r--sugar_network/node/master.py5
-rw-r--r--sugar_network/node/routes.py110
-rw-r--r--sugar_network/node/slave.py28
-rw-r--r--sugar_network/toolkit/__init__.py25
-rw-r--r--sugar_network/toolkit/coroutine.py1
-rw-r--r--sugar_network/toolkit/http.py185
-rw-r--r--sugar_network/toolkit/router.py54
-rw-r--r--tests/__init__.py63
-rw-r--r--tests/data/d820a3405d6aadf2cf207f6817db2a79f8fa07aa15
-rw-r--r--tests/data/f470db873b6a35903aca1f2492188e1c4b9ffc4215
-rw-r--r--tests/data/owner.key12
-rw-r--r--tests/data/owner.key.pub1
-rwxr-xr-xtests/integration/master_personal.py14
-rwxr-xr-xtests/integration/master_slave.py12
-rwxr-xr-xtests/integration/node_client.py15
-rwxr-xr-xtests/integration/node_packages.py12
-rwxr-xr-xtests/units/client/offline_routes.py2
-rwxr-xr-xtests/units/client/online_routes.py5
-rwxr-xr-xtests/units/client/routes.py49
-rwxr-xr-xtests/units/client/server_routes.py13
-rwxr-xr-xtests/units/model/comment.py4
-rwxr-xr-xtests/units/model/review.py5
-rwxr-xr-xtests/units/model/solution.py5
-rwxr-xr-xtests/units/node/node.py109
-rwxr-xr-xtests/units/node/sync_offline.py4
-rwxr-xr-xtests/units/node/sync_online.py56
-rwxr-xr-xtests/units/toolkit/router.py2
39 files changed, 592 insertions, 678 deletions
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 <http://www.gnu.org/licenses/>.
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 <http://www.gnu.org/licenses/>.
+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 '<Connection api_url=%s>' % 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())