summaryrefslogtreecommitdiff
path: root/qvisqve
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2018-02-09 13:53:33 +0200
committerLars Wirzenius <liw@liw.fi>2018-02-09 13:56:43 +0200
commitf1f291b270b96fe1511286cb807f02c9741b0d71 (patch)
tree509a7f697685e9282fbdc6838fb037ff6cb5e4dd /qvisqve
parent3b208da0461f5a129fcbc527fbfdd9ed8309d077 (diff)
downloadqvisqve-f1f291b270b96fe1511286cb807f02c9741b0d71.tar.gz
Rename: to Qvisqve
Diffstat (limited to 'qvisqve')
-rw-r--r--qvisqve/__init__.py31
-rw-r--r--qvisqve/api.py49
-rw-r--r--qvisqve/app.py76
-rw-r--r--qvisqve/backend.py20
-rw-r--r--qvisqve/log_setup.py54
-rw-r--r--qvisqve/responses.py59
-rw-r--r--qvisqve/router.py23
-rw-r--r--qvisqve/token.py72
-rw-r--r--qvisqve/token_router.py113
-rw-r--r--qvisqve/version.py2
-rw-r--r--qvisqve/version_router.py36
11 files changed, 535 insertions, 0 deletions
diff --git a/qvisqve/__init__.py b/qvisqve/__init__.py
new file mode 100644
index 0000000..a1d80f9
--- /dev/null
+++ b/qvisqve/__init__.py
@@ -0,0 +1,31 @@
+# Copyright (C) 2017 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/>.
+
+from .version import __version__, __version_info__
+from .responses import (
+ bad_request_response,
+ created_response,
+ ok_response,
+ unauthorized_response,
+)
+from .log_setup import setup_logging, log
+from .token import TokenGenerator
+
+from .router import Router
+from .version_router import VersionRouter
+from .token_router import TokenRouter
+
+from .api import API
+from .app import create_app
diff --git a/qvisqve/api.py b/qvisqve/api.py
new file mode 100644
index 0000000..2dee954
--- /dev/null
+++ b/qvisqve/api.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2017 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 qvisqve
+
+
+class API:
+
+ def __init__(self, config):
+ self._config = config
+
+ def find_missing_route(self, path):
+ qvisqve.log.log('info', msg_text='find_missing_route', path=path)
+
+ routers = [
+ qvisqve.VersionRouter(),
+ qvisqve.TokenRouter(
+ self._create_token_generator(), self._get_clients()),
+ ]
+
+ routes = []
+ for router in routers:
+ routes.extend(router.get_routes())
+
+ return routes
+
+ def _create_token_generator(self):
+ tg = qvisqve.TokenGenerator()
+ cfg = self._config
+ tg.set_issuer(cfg['token-issuer'])
+ tg.set_lifetime(cfg['token-lifetime'])
+ tg.set_signing_key(cfg['token-private-key'])
+ return tg
+
+ def _get_clients(self):
+ return self._config.get('clients', {})
diff --git a/qvisqve/app.py b/qvisqve/app.py
new file mode 100644
index 0000000..e608a39
--- /dev/null
+++ b/qvisqve/app.py
@@ -0,0 +1,76 @@
+# Copyright (C) 2017-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 os
+
+
+import apifw
+import slog
+import yaml
+
+
+import qvisqve
+
+
+DEFAULT_CONFIG_FILE = '/dev/null'
+
+
+def dict_logger(log, stack_info=None):
+ qvisqve.log.log(exc_info=stack_info, **log)
+
+
+def read_config(filename):
+ with open(filename) as f:
+ return yaml.safe_load(f)
+
+
+def check_config(cfg):
+ for key in cfg:
+ if cfg[key] is None:
+ raise Exception('Configration %s should not be None' % key)
+
+
+_counter = slog.Counter()
+
+
+def counter():
+ new_context = 'HTTP transaction {}'.format(_counter.increment())
+ qvisqve.log.set_context(new_context)
+
+
+default_config = {
+ 'log': [],
+ 'token-issuer': None,
+ 'token-public-key': None,
+ 'token-private-key': None,
+ 'token-lifetime': None,
+ 'clients': None,
+}
+
+
+def create_app():
+ config_filename = os.environ.get('QVISQVE_CONFIG', DEFAULT_CONFIG_FILE)
+ actual_config = read_config(config_filename)
+ config = dict(default_config)
+ config.update(actual_config or {})
+ if 'token-audience' not in config:
+ config['token-audience'] = config.get('token-issuer')
+ check_config(config)
+ qvisqve.setup_logging(config)
+ qvisqve.log.log('info', msg_text='Qvisqve starting')
+
+ api = qvisqve.API(config)
+ return apifw.create_bottle_application(api, counter, dict_logger, config)
diff --git a/qvisqve/backend.py b/qvisqve/backend.py
new file mode 100644
index 0000000..3db3225
--- /dev/null
+++ b/qvisqve/backend.py
@@ -0,0 +1,20 @@
+# Copyright (C) 2017-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 qvisqve
+
+
+app = qvisqve.create_app()
diff --git a/qvisqve/log_setup.py b/qvisqve/log_setup.py
new file mode 100644
index 0000000..e32137c
--- /dev/null
+++ b/qvisqve/log_setup.py
@@ -0,0 +1,54 @@
+# Copyright (C) 2017 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 slog
+
+
+def drop_get_message(log_obj):
+ # These are useless and annoying in gunicorn log messages.
+ if 'getMessage' in log_obj:
+ del log_obj['getMessage']
+ return log_obj
+
+
+# We are probably run under gunicorn, which sets up logging via the
+# logging library. Hijack that so actual logging happens via the slog
+# library. For this, we need to know the logger names gunicorn uses.
+gunicorn_loggers = ['gunicorn.access', 'gunicorn.error']
+
+
+# This sets up a global log variable that doesn't actually log
+# anything anywhere. This is useful so that code can unconditionally
+# call log.log(...) from anywhere. See setup_logging() for setting up
+# actual logging to somewhere persistent.
+
+log = slog.StructuredLog()
+log.add_log_writer(slog.NullSlogWriter(), slog.FilterAllow())
+log.add_log_massager(drop_get_message)
+slog.hijack_logging(log, logger_names=gunicorn_loggers)
+
+
+def setup_logging(config):
+ for target in config.get('log', []):
+ setup_logging_to_file(target, slog.FilterAllow())
+
+
+def setup_logging_to_file(target, rule):
+ writer = slog.FileSlogWriter()
+ writer.set_filename(target['filename'])
+ if 'max_bytes' in target:
+ writer.set_max_file_size(target['max_bytes'])
+ log.add_log_writer(writer, rule)
diff --git a/qvisqve/responses.py b/qvisqve/responses.py
new file mode 100644
index 0000000..281ec43
--- /dev/null
+++ b/qvisqve/responses.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2017 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 apifw
+
+
+def response(status, body, headers):
+ return apifw.Response(
+ {
+ 'status': status,
+ 'body': body,
+ 'headers': headers,
+ }
+ )
+
+
+def ok_response(body, headers=None):
+ if headers is None:
+ headers = {}
+ if 'Content-Type' not in headers:
+ headers.update({
+ 'Content-Type': 'application/json',
+ })
+ return response(apifw.HTTP_OK, body, headers)
+
+
+def created_response(body, location):
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Location': location,
+ }
+ return response(apifw.HTTP_CREATED, body, headers)
+
+
+def bad_request_response(body):
+ headers = {
+ 'Content-Type': 'text/plain',
+ }
+ return response(apifw.HTTP_BAD_REQUEST, body, headers)
+
+
+def unauthorized_response(body):
+ headers = {
+ 'Content-Type': 'text/plain',
+ }
+ return response(apifw.HTTP_UNAUTHORIZED, body, headers)
diff --git a/qvisqve/router.py b/qvisqve/router.py
new file mode 100644
index 0000000..9f171b0
--- /dev/null
+++ b/qvisqve/router.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2017 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/>.
+
+
+class Router:
+
+ def __init__(self):
+ pass
+
+ def get_routes(self):
+ raise NotImplementedError()
diff --git a/qvisqve/token.py b/qvisqve/token.py
new file mode 100644
index 0000000..3b343c2
--- /dev/null
+++ b/qvisqve/token.py
@@ -0,0 +1,72 @@
+#!/usr/bin/python3
+# Copyright (C) 2017-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 time
+
+
+import Crypto.PublicKey.RSA
+import jwt
+
+
+import qvisqve
+
+
+class TokenGenerator:
+
+ _algorithm = 'RS512'
+
+ def __init__(self):
+ self._issuer = None
+ self._lifetime = None
+ self._key = None
+
+ def set_issuer(self, issuer):
+ self._issuer = issuer
+ qvisqve.log.log('info', msg_text='Set issuer', issuer=issuer)
+
+ def set_lifetime(self, lifetime):
+ self._lifetime = lifetime
+ qvisqve.log.log(
+ 'info', msg_text='Set token lifetime', lifetime=lifetime)
+
+ def set_signing_key(self, key):
+ imported_key = Crypto.PublicKey.RSA.importKey(key)
+ self._key = imported_key.exportKey('PEM')
+ qvisqve.log.log(
+ 'info', msg_text='Set signing key', key=self._key,
+ orig_key=key, imported_key=imported_key)
+
+ def new_token(self, audience, scope):
+ assert self._issuer is not None
+ assert self._lifetime is not None
+ assert self._key is not None
+
+ now = time.time()
+ claims = {
+ 'iss': self._issuer,
+ 'sub': '',
+ 'aud': audience,
+ 'exp': now + self._lifetime,
+ 'scope': scope,
+ }
+
+ token = jwt.encode(
+ claims,
+ self._key,
+ algorithm=self._algorithm)
+
+ return token.decode('ascii')
diff --git a/qvisqve/token_router.py b/qvisqve/token_router.py
new file mode 100644
index 0000000..9acc924
--- /dev/null
+++ b/qvisqve/token_router.py
@@ -0,0 +1,113 @@
+# 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 urllib.parse
+
+
+import bottle
+
+
+import qvisqve
+import qvisqve_secrets
+
+
+class TokenRouter(qvisqve.Router):
+
+ def __init__(self, token_generator, clients):
+ super().__init__()
+ self._generator = token_generator
+ self._clients = Clients(clients)
+
+ def get_routes(self):
+ return [
+ {
+ 'method': 'POST',
+ 'path': '/token',
+ 'callback': self._create_token,
+ 'needs-authorization': False,
+ },
+ ]
+
+ def _create_token(self, content_type, body, **kwargs):
+ qvisqve.log.log('xxx', body=body, kwargs=kwargs)
+
+ if content_type != 'application/x-www-form-urlencoded':
+ return qvisqve.bad_request_response('Wrong content type')
+
+ client_id, client_secret = bottle.request.auth
+ if not self._clients.is_correct_secret(client_id, client_secret):
+ return qvisqve.unauthorized_response('Unauthorized')
+
+ params = self._get_form_params(body)
+
+ grant_type = self._get_grant_type(params)
+ if grant_type != 'client_credentials':
+ return qvisqve.bad_request_response('Wrong grant type')
+
+ scope = self._get_scope(params)
+ if scope is None:
+ return qvisqve.bad_request_response('Bad scope')
+
+ allowed = self._clients.get_allowed_scopes(client_id)
+ scope = ' '.join(
+ s
+ for s in scope.split()
+ if s in allowed
+ )
+
+ token = self._generator.new_token(client_id, scope)
+ return qvisqve.ok_response({
+ 'access_token': token,
+ 'token_type': 'bearer',
+ 'scope': scope,
+ })
+
+ def _get_form_params(self, body):
+ body = body.decode('UTF-8')
+ return urllib.parse.parse_qs(body)
+
+ def _get_grant_type(self, params):
+ grant_type = params.get('grant_type')
+ if len(grant_type) == 1:
+ return grant_type[0]
+ return None
+
+ def _get_scope(self, params):
+ scope = params.get('scope', [])
+ if len(scope) > 1:
+ return None
+ if scope:
+ return scope[0]
+ return ''
+
+
+class Clients:
+
+ def __init__(self, clients):
+ self._clients = clients
+ self._hasher = qvisqve_secrets.SecretHasher()
+
+ def is_correct_secret(self, client_id, cleartext):
+ client = self._get_client(client_id)
+ secret = client.get('client_secret')
+ return secret and self._hasher.is_correct(secret, cleartext)
+
+ def get_allowed_scopes(self, client_id):
+ client = self._get_client(client_id)
+ return client.get('allowed_scopes', [])
+
+ def _get_client(self, client_id):
+ return self._clients.get(client_id, {})
diff --git a/qvisqve/version.py b/qvisqve/version.py
new file mode 100644
index 0000000..456773e
--- /dev/null
+++ b/qvisqve/version.py
@@ -0,0 +1,2 @@
+__version__ = "0.8+git"
+__version_info__ = (0, 8, '+git')
diff --git a/qvisqve/version_router.py b/qvisqve/version_router.py
new file mode 100644
index 0000000..311d505
--- /dev/null
+++ b/qvisqve/version_router.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2017 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 qvisqve
+
+
+class VersionRouter(qvisqve.Router):
+
+ def get_routes(self):
+ return [
+ {
+ 'method': 'GET',
+ 'path': '/version',
+ 'callback': self._version,
+ 'needs-authorization': False,
+ },
+ ]
+
+ def _version(self, *args, **kwargs):
+ version = {
+ 'version': qvisqve.__version__,
+ }
+ return qvisqve.ok_response(version)