diff options
Diffstat (limited to 'sugar_network/node/routes.py')
-rw-r--r-- | sugar_network/node/routes.py | 110 |
1 files changed, 57 insertions, 53 deletions
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 |