import logging import sys import Crypto.PublicKey.RSA import bottle import jwt class TokenParser: def __init__(self, pubkey): self._pubkey = pubkey def parse_token(self, token_text): return jwt.decode( token_text, key=self._pubkey.exportKey('OpenSSH'), audience=None, options={'verify_aud': False}) class AccessChecker: def __init__(self, pubkey): self._parser = TokenParser(pubkey) def access_is_allowed(self, headers, required_scopes): token = self._get_token(headers) logging.debug('Access token %r', token) if token is None and len(required_scopes) != 0: logging.error('No valid access token') return False if token: scopes = token.get('scope', '').split() missing = set(required_scopes).difference(scopes) if missing: logging.error( 'Required scopes that are missing from token: %r', missing) return False return True def _get_token(self, headers): token_text = self._get_token_text(headers) if token_text is None: return None return self._parser.parse_token(token_text) def _get_token_text(self, headers): v = headers.get('Authorization', '') words = v.split() if len(words) == 2: keyword, token_text = words if keyword.lower() == 'bearer': return token_text class API: def __init__(self, app, token_pubkey): self._checker = AccessChecker(token_pubkey) self._add_routes(app, self.get_routes()) def get_routes(self): raise NotImplementedError() def _add_routes(self, app, routes): for route in routes: func = route.pop('func') scopes = route.pop('scopes') callback = lambda **kwargs: self.check(func, scopes, kwargs) route = dict(route) route['callback'] = callback app.route(**route) def check(self, func, required_scopes, kwargs): r = bottle.request logging.debug('Checking access for request %s %s', r.method, r.path) if self._checker.access_is_allowed(bottle.request.headers, required_scopes): logging.info('Serving request %s %s', r.method, r.path) ret = func(**kwargs) logging.info('Result: %r', ret) return ret logging.error('Request denied %s %s', r.method, r.path) return bottle.HTTPError(400) class Controller(API): def get_routes(self): return [ { 'method': 'GET', 'path': '/status', 'func': self._status, 'scopes': ['status'], }, { 'method': 'GET', 'path': '/hello/', 'func': self._hello, 'scopes': ['hello'], }, ] def _status(self): return { 'queue': [], 'running': [], 'finished': [], } def _hello(self, name=None): return 'hello {}\n'.format(name) def get_key_from_file(filename): with open(filename) as f: key_text = f.read() return Crypto.PublicKey.RSA.importKey(key_text) def main(): logging.basicConfig( filename='api.log', level=logging.DEBUG, format='%(levelname)s %(message)s') filename = sys.argv[1] key = get_key_from_file(filename) app = bottle.Bottle() api = Controller(app, key) app.run(host='localhost', port=2222) main()