# Copyright (C) 2012 Aleksey Lim # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import logging import hashlib import tempfile from os.path import exists, join import active_document as ad from sugar_network import node from sugar_network.node.sync_master import SyncCommands from sugar_network.node import auth from sugar_network.resources.volume import Commands, VolumeCommands from sugar_network.toolkit import router from active_toolkit import util, enforce _DEFAULT_MASTER_GUID = 'api-testing.network.sugarlabs.org' _logger = logging.getLogger('node.commands') class NodeCommands(VolumeCommands, Commands): def __init__(self, volume): VolumeCommands.__init__(self, volume) Commands.__init__(self) self._is_master = False node_path = join(volume.root, 'node') master_path = join(volume.root, 'master') if exists(node_path): with file(node_path) as f: self._guid = f.read().strip() elif exists(master_path): with file(master_path) as f: self._guid = f.read().strip() self._is_master = True else: self._guid = ad.uuid() with file(node_path, 'w') as f: f.write(self._guid) if not self._is_master and not exists(master_path): with file(master_path, 'w') as f: f.write(_DEFAULT_MASTER_GUID) @property def is_master(self): return self._is_master def connect(self, callback, condition=None, **kwargs): self.volume.connect(callback, condition) @ad.volume_command(method='GET', mime_type='text/html') def hello(self): return _HELLO_HTML @ad.volume_command(method='GET', cmd='stat', mime_type='application/json') def stat(self): return {'guid': self._guid, 'master': self._is_master, 'seqno': self.volume.seqno.value, } @ad.document_command(method='DELETE', permissions=ad.ACCESS_AUTH | ad.ACCESS_AUTHOR) def delete(self, document, guid): # Servers data should not be deleted immediately # to let master-node synchronization possible directory = self.volume[document] directory.update(guid, {'layer': ['deleted']}) def resolve(self, request): cmd = VolumeCommands.resolve(self, request) if cmd is None: return if cmd.permissions & ad.ACCESS_AUTH: enforce(auth.try_validate(request, 'user'), router.Unauthorized, 'User is not authenticated') if cmd.permissions & ad.ACCESS_AUTHOR and 'guid' in request: doc = self.volume[request['document']].get(request['guid']) enforce(request.principal in doc['user'] or auth.try_validate(request, 'root'), ad.Forbidden, 'Operation is permitted only for authors') return cmd @ad.document_command(method='PUT', cmd='attach', permissions=ad.ACCESS_AUTH) def attach(self, document, guid, request): auth.validate(request, 'root') directory = self.volume[document] doc = directory.get(guid) # TODO Reading layer here is a race layer = list(set(doc['layer']) | set(request.content)) directory.update(guid, {'layer': layer}) @ad.document_command(method='PUT', cmd='detach', permissions=ad.ACCESS_AUTH) def detach(self, document, guid, request): auth.validate(request, 'root') directory = self.volume[document] doc = directory.get(guid) # TODO Reading layer here is a race layer = list(set(doc['layer']) - set(request.content)) directory.update(guid, {'layer': layer}) @ad.document_command(method='GET', cmd='feed', mime_type='application/json') def feed(self, guid, layer, request): result = [] impls, __ = self.volume['implementation'].find( limit=ad.MAX_LIMIT, context=guid, layer=layer) for impl in impls: for arch, spec in impl['spec'].items(): spec['guid'] = impl.guid spec['version'] = impl['version'] spec['arch'] = arch spec['stability'] = impl['stability'] result.append(spec) return result def before_create(self, request, props): if request['document'] == 'user': props['guid'], props['pubkey'] = _load_pubkey(props['pubkey']) else: props['user'] = [request.principal] self._set_author(props) if self._is_master and 'implement' in props: implement = props['implement'] if not isinstance(implement, basestring): implement = implement[0] props['guid'] = implement VolumeCommands.before_create(self, request, props) def before_update(self, request, props): if 'user' in props: self._set_author(props) VolumeCommands.before_update(self, request, props) @ad.directory_command_pre(method='GET') def _NodeCommands_find_pre(self, request): if 'limit' not in request: request['limit'] = node.find_limit.value elif request['limit'] > node.find_limit.value: _logger.warning('The find limit is restricted to %s', node.find_limit.value) request['limit'] = node.find_limit.value layer = request.get('layer', ['public']) if 'deleted' in layer: _logger.warning('Requesting "deleted" layer') layer.remove('deleted') request['layer'] = layer @ad.document_command_post(method='GET') def _NodeCommands_get_post(self, request, response, result): directory = self.volume[request['document']] doc = directory.get(request['guid']) enforce('deleted' not in doc['layer'], ad.NotFound, 'Document deleted') return result def _set_author(self, props): users = self.volume['user'] authors = [] for user_guid in props['user']: if not users.exists(user_guid): _logger.warning('No %r user to set author property', user_guid) continue user = users.get(user_guid) if user['name']: authors.append(user['name']) props['author'] = authors class MasterCommands(NodeCommands, SyncCommands): def __init__(self, volume): NodeCommands.__init__(self, volume) SyncCommands.__init__(self) @ad.document_command(method='PUT', cmd='merge', permissions=ad.ACCESS_AUTH) def merge(self, document, guid, request): auth.validate(request, 'root') directory = self.volume[document] directory.merge(guid, request.content) @ad.volume_command(method='GET', cmd='whoami', mime_type='application/json') def whoami(self, request): roles = [] if auth.try_validate(request, 'root'): roles.append('root') return {'roles': roles, 'user': request.principal} def _load_pubkey(pubkey): pubkey = pubkey.strip() try: with tempfile.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 = util.assert_call( ['ssh-keygen', '-f', key_file.name, '-e', '-m', 'PKCS8']) except Exception: message = 'Cannot read DSS public key gotten for registeration' util.exception(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 ad.Forbidden(message) return str(hashlib.sha1(pubkey.split()[1]).hexdigest()), pubkey_pkcs8 _HELLO_HTML = """\

Welcome to Sugar Network API!

Consult Sugar Labs Wiki to learn how it can be used. """