diff options
-rw-r--r-- | apifw.yarn | 13 | ||||
-rw-r--r-- | apifw/bottleapp.py | 34 | ||||
-rw-r--r-- | apitest.py | 11 | ||||
-rwxr-xr-x | create-token | 2 | ||||
-rw-r--r-- | pylint.conf | 1 | ||||
-rwxr-xr-x | run-apitest | 9 |
6 files changed, 62 insertions, 8 deletions
@@ -37,6 +37,11 @@ It's a silly name. Please suggest something better. THEN HTTP status code is 200 OK AND HTTP body is "version: 4.2" + WHEN client gets an authorization token with scope "uapi_upload_put" + AND client uploads a fake jpg + THEN HTTP status code is 200 OK + AND HTTP body is "thank you for fake jpg" + FINALLY stop apitest @@ -80,6 +85,14 @@ It's a silly name. Please suggest something better. curl -sv -H "Authorization: Bearer $token" \ "http://127.0.0.1:12765/version" > "$DATADIR/out" 2> "$DATADIR/err" + IMPLEMENTS WHEN client uploads a fake jpg + token="$(cat "$DATADIR/token")" + curl -sv -H "Authorization: Bearer $token" \ + -H "Content-type: application/jpeg" \ + -d "fake jpg" \ + -X PUT \ + "http://127.0.0.1:12765/upload" > "$DATADIR/out" 2> "$DATADIR/err" + IMPLEMENTS WHEN client gets an authorization token with scope "(.+)" iss="$(cat "$DATADIR/iss")" aud="$(cat "$DATADIR/aud")" diff --git a/apifw/bottleapp.py b/apifw/bottleapp.py index 60e3944..9002f02 100644 --- a/apifw/bottleapp.py +++ b/apifw/bottleapp.py @@ -14,6 +14,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. +import logging import re import time @@ -100,17 +101,22 @@ class BottleAuthorizationPlugin: value = request.get_header('Authorization', '') if not value: self.raise_unauthorized('No Authorization header') + logging.debug('Request has Authorization header: good') return value def parse_authorization_header(self, value): words = value.split() if len(words) != 2 or words[0].lower() != 'bearer': self.raise_unauthorized('Authorization should be "Bearer TOKEN"') + logging.debug( + 'Request Authorization header looks like a bearer token: good') return words[1] def parse_token(self, token): try: - return apifw.decode_token(token, self.pubkey, audience=self.aud) + token = apifw.decode_token(token, self.pubkey, audience=self.aud) + logging.debug('Request Authorization token can be decoded: good') + return token except jwt.InvalidTokenError as e: self.raise_unauthorized(str(e)) @@ -118,11 +124,18 @@ class BottleAuthorizationPlugin: if claims['iss'] != self.iss: self.raise_unauthorized( 'Expected issuer %s, got %s' % (self.iss, claims['iss'])) + logging.debug('Token issuer is correct: good') def scope_allows_route(self, claim_scopes, route): scopes = claim_scopes.split(' ') route_scope = self.get_scope_for_route(route['method'], route['rule']) - return route_scope in scopes + if route_scope in scopes: + logging.debug( + 'Route scope %s is in scopes %r', route_scope, scopes) + return True + logging.error( + 'Route scope %s is NOT in scopes %r', route_scope, scopes) + return False def get_scope_for_route(self, method, rule): scope = re.sub(self.route_pat, 'id', rule) @@ -160,15 +173,30 @@ class BottleApplication: routes = self._api.find_missing_route(bottle.request.path) if routes: for route in routes: + callback = self._callback_with_body(route['callback']) route_dict = { 'method': route.get('method', 'GET'), 'path': route['path'], - 'callback': route['callback'], + 'callback': callback, } self._bottleapp.route(**route_dict) else: raise + def _callback_with_body(self, callback): + def wrapper(*args, **kwargs): + content_type, body = self._get_request_body() + return callback(content_type, body, *args, **kwargs) + return wrapper + + def _get_request_body(self): + content_type = bottle.request.get_header('Content-Type') + if content_type == 'application/json': + body = bottle.request.json + else: + body = bottle.request.body.read() + return content_type, body + def create_bottle_application(api, logger, config): # Create a new bottle.Bottle application, set it up, and return it @@ -39,12 +39,19 @@ class Api(apifw.Api): 'path': '/version', 'callback': self.version, }, + { + 'method': 'PUT', + 'path': '/upload', + 'callback': self.upload, + }, ] - def version(self): - logging.info('/version called!\n') + def version(self, content_type, body): return 'version: 4.2' + def upload(self, content_type, body): + return 'thank you for %s\n' % body.decode('ascii') + # We want logging. gunicorn provides logging, but only of its own # stuff, and if we log something ourselves, using logging.debug and diff --git a/create-token b/create-token index c0e43a6..e2e9fbb 100755 --- a/create-token +++ b/create-token @@ -26,7 +26,7 @@ import apifw filename = sys.argv[1] iss = sys.argv[2] aud = sys.argv[3] -scopes = sys.argv[4] +scopes = ' '.join(sys.argv[4].split()) key_text = open(filename, 'r').read() key = Crypto.PublicKey.RSA.importKey(key_text) diff --git a/pylint.conf b/pylint.conf index 369aad6..801baae 100644 --- a/pylint.conf +++ b/pylint.conf @@ -5,6 +5,7 @@ persistent=no disable= invalid-name, missing-docstring, + no-member, no-self-use, redefined-variable-type, too-few-public-methods, diff --git a/run-apitest b/run-apitest index 0af9b63..4455e33 100755 --- a/run-apitest +++ b/run-apitest @@ -34,9 +34,14 @@ export APITEST_ISS=testissuer export APITEST_AUD=aud export APITEST_LOG="$log" +scopes=" +uapi_version_get +uapi_upload_put +" + ./generate-rsa-key "$tmp/key" -./create-token "$tmp/key" "$APITEST_ISS" "$APITEST_AUD" uapi_version_get > "$token" +./create-token "$tmp/key" "$APITEST_ISS" "$APITEST_AUD" "$scopes" > "$token" export APITEST_PUBKEY="$(cat "$tmp/key.pub")" -gunicorn --bind 127.0.0.1:12765 -p "$tmp/pid" -w16 \ +gunicorn --bind 127.0.0.1:12765 -p "$tmp/pid" -w1 \ --log-file "$log" --log-level=debug apitest:app |