diff options
Diffstat (limited to 'muck_poc')
-rwxr-xr-x | muck_poc | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/muck_poc b/muck_poc new file mode 100755 index 0000000..40f59ba --- /dev/null +++ b/muck_poc @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# Copyright (C) 2018 Lars Wirzenius +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import json +import logging +import os +import sys + +import bottle + +import muck + + +class MuckAPI: + + def __init__(self, bottleapp, store, signing_key_text): + self._add_routes(bottleapp) + self._store = store + self._gen = muck.IdGenerator() + self._ac = muck.AuthorizationChecker(signing_key_text) + + def _add_routes(self, bottleapp): + routes = [ + { + 'method': 'POST', + 'path': '/res', + 'callback': self._check_authz( + 'POST', 'create', self._create_res), + }, + { + 'method': 'PUT', + 'path': '/res', + 'callback': self._check_authz( + 'PUT', 'update', self._update_res), + }, + { + 'method': 'GET', + 'path': '/res', + 'callback': self._check_authz( + 'GET', 'show', self._show_res), + }, + { + 'method': 'DELETE', + 'path': '/res', + 'callback': self._check_authz( + 'DELETE', 'delete', self._delete_res), + }, + { + 'method': 'GET', + 'path': '/search', + 'callback': self._check_authz( + 'GET', 'show', self._search_res), + }, + ] + + for route in routes: + bottleapp.route(**route) + + def _check_authz(self, req_method, req_scope, callback): + def check_authz(): + r = muck.Request(method=bottle.request.method) + r.add_headers(bottle.request.headers) + if self._ac.request_is_allowed(r, req_method, [req_scope]): + return callback() + logging.error('Access denied') + return bottle.HTTPError(401) + + return check_authz + + def _create_res(self): + res = self._get_json_body() + meta = { + 'id': self._gen.new_id(), + 'rev': self._gen.new_id(), + } + create = muck.CreateChange(meta, res) + self._store.change(create) + return self._create_response(201, 'create', meta, res) + + def _update_res(self): + rid = self._get_resource_id() + try: + meta, _ = self._get_existing(rid) + except bottle.HTTPError as e: + return e + + rev = self._get_resource_revision() + if meta['rev'] != rev: + return bottle.HTTPError(status=400, body='Wrong revision') + + res = self._get_json_body() + meta['rev'] = self._gen.new_id() + update = muck.UpdateChange(meta, res) + self._store.change(update) + return self._create_response(200, 'change', meta, res) + + def _show_res(self): + rid = self._get_resource_id() + try: + meta, res = self._get_existing(rid) + except bottle.HTTPError as e: + return e + return self._create_response(200, 'show', meta, res) + + def _delete_res(self): + rid = self._get_resource_id() + try: + meta, res = self._get_existing(rid) + except bottle.HTTPError as e: + return e + delete = muck.DeleteChange(meta, res) + self._store.change(delete) + return self._create_response(200, 'delete', meta, res) + + def _search_res(self): + body = self._get_json_body() + cond = body.get('cond') + ms = self._store.get_memory_store() + result = { + 'resources': ms.search(cond), + } + return bottle.HTTPResponse(status=200, body=json.dumps(result)) + + def _get_json_body(self): + f = bottle.request.body + body = f.read() + return json.loads(body, encoding='UTF-8') + + def _get_resource_id(self): + return bottle.request.headers.get('Muck-Id') + + def _get_resource_revision(self): + return bottle.request.headers.get('Muck-Revision') + + def _get_existing(self, rid): + ms = self._store.get_memory_store() + if rid not in ms: + logging.warning('ERROR: {}: not found'.format(rid)) + raise bottle.HTTPResponse(status=404) + + return ms[rid] + + def _create_response(self, status, operation, meta, res): + headers = self._meta_headers(meta) + return bottle.HTTPResponse( + status=status, headers=headers, body=json.dumps(res)) + + def _meta_headers(self, meta): + return { + 'Muck-ID': meta['id'], + 'Muck-Revision': meta['rev'], + } + + +with open(sys.argv[1]) as f: + config = json.load(f) + +logging.basicConfig( + filename=config['log'], level=logging.DEBUG, + format='%(levelname)s %(message)s') + +logging.info('Muck starts') + +signing_key_text = open(config['signing-key-filename']).read() +store = muck.Store(config['store']) + +pid = os.getpid() +with open(config['pid'], 'w') as f: + f.write(str(pid)) + +app = bottle.default_app() +api = MuckAPI(app, store, signing_key_text) +bottle.run(host='127.0.0.1', port=12765) |