summaryrefslogtreecommitdiff
path: root/muck_poc
diff options
context:
space:
mode:
Diffstat (limited to 'muck_poc')
-rwxr-xr-xmuck_poc186
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)