diff --git a/misc/BreCal.postman_collection.json b/misc/BreCal.postman_collection.json index c91ad79..b8bd91e 100644 --- a/misc/BreCal.postman_collection.json +++ b/misc/BreCal.postman_collection.json @@ -2,10 +2,52 @@ "info": { "_postman_id": "9242b2d1-196b-4b2e-af57-c0e9eb141dba", "name": "BreCal", + "description": "Bremen Calling relevant API calls", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "10427908" }, "item": [ + { + "name": "Login user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.environment.set(\"LOGON_TOKEN\", responseBody)" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"username\" : \"Londo\",\r\n \"password\" : \"Hallowach\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://{{PATH}}/login", + "protocol": "http", + "host": [ + "{{PATH}}" + ], + "path": [ + "login" + ] + } + }, + "response": [] + }, { "name": "Participant GET", "request": { @@ -14,7 +56,7 @@ "bearer": [ { "key": "token", - "value": "xxxTest", + "value": "{{LOGON_TOKEN}}", "type": "string" } ] @@ -48,7 +90,7 @@ "bearer": [ { "key": "token", - "value": "xxxTest", + "value": "{{LOGON_TOKEN}}", "type": "string" } ] @@ -76,7 +118,7 @@ "bearer": [ { "key": "token", - "value": "xxxTest", + "value": "{{LOGON_TOKEN}}", "type": "string" } ] @@ -113,7 +155,7 @@ "bearer": [ { "key": "token", - "value": "xxxTest", + "value": "{{LOGON_TOKEN}}", "type": "string" } ] @@ -150,7 +192,7 @@ "bearer": [ { "key": "token", - "value": "xxxTest", + "value": "{{LOGON_TOKEN}}", "type": "string" } ] @@ -173,6 +215,16 @@ { "name": "Notifications GET", "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{LOGON_TOKEN}}", + "type": "string" + } + ] + }, "method": "GET", "header": [], "url": { @@ -202,7 +254,7 @@ "bearer": [ { "key": "token", - "value": "xxxTest", + "value": "{{LOGON_TOKEN}}", "type": "string" } ] @@ -230,7 +282,7 @@ "bearer": [ { "key": "token", - "value": "xxxTest", + "value": "{{LOGON_TOKEN}}", "type": "string" } ] @@ -264,7 +316,7 @@ "bearer": [ { "key": "token", - "value": "xxxTest", + "value": "{{LOGON_TOKEN}}", "type": "string" } ] @@ -301,7 +353,7 @@ "bearer": [ { "key": "token", - "value": "xxxTest", + "value": "{{LOGON_TOKEN}}", "type": "string" } ] @@ -333,7 +385,7 @@ "bearer": [ { "key": "token", - "value": "xxxTest", + "value": "{{LOGON_TOKEN}}", "type": "string" } ] @@ -359,5 +411,28 @@ }, "response": [] } + ], + "auth": { + "type": "bearer" + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } ] } \ No newline at end of file diff --git a/src/server/BreCal/__init__.py b/src/server/BreCal/__init__.py index f61244f..bdc004b 100644 --- a/src/server/BreCal/__init__.py +++ b/src/server/BreCal/__init__.py @@ -2,7 +2,6 @@ from flask import Flask import os import logging -import secrets from . import local_db from .api import shipcalls @@ -48,6 +47,3 @@ def create_app(test_config=None): logging.info('App started') return app - -def create_api_key(): - return secrets.token_urlsafe(16) \ No newline at end of file diff --git a/src/server/BreCal/api/berths.py b/src/server/BreCal/api/berths.py index 70ff383..6272af9 100644 --- a/src/server/BreCal/api/berths.py +++ b/src/server/BreCal/api/berths.py @@ -1,12 +1,15 @@ from flask import Blueprint, request from webargs.flaskparser import parser from .. import impl +from ..services.auth_guard import auth_guard import json + bp = Blueprint('berths', __name__) @bp.route('/berths', methods=['get']) +@auth_guard() # no restriction by role def GetBerths(): if 'Authorization' in request.headers: diff --git a/src/server/BreCal/api/notifications.py b/src/server/BreCal/api/notifications.py index 420d1bc..a8a08ee 100644 --- a/src/server/BreCal/api/notifications.py +++ b/src/server/BreCal/api/notifications.py @@ -1,5 +1,6 @@ from flask import Blueprint, request from .. import impl +from ..services.auth_guard import auth_guard import logging import json @@ -7,6 +8,7 @@ bp = Blueprint('notifications', __name__) @bp.route('/notifications', methods=['get']) +@auth_guard() # no restriction by role def GetNotifications(): # TODO: verify token diff --git a/src/server/BreCal/api/participant.py b/src/server/BreCal/api/participant.py index 1566b38..2d6b0f2 100644 --- a/src/server/BreCal/api/participant.py +++ b/src/server/BreCal/api/participant.py @@ -1,10 +1,12 @@ from flask import Blueprint, request from .. import impl +from ..services.auth_guard import auth_guard import json bp = Blueprint('participant', __name__) @bp.route('/participant', methods=['get']) +@auth_guard() # no restriction by role def GetParticipant(): if 'Authorization' in request.headers: diff --git a/src/server/BreCal/api/shipcalls.py b/src/server/BreCal/api/shipcalls.py index 80ff381..59f806d 100644 --- a/src/server/BreCal/api/shipcalls.py +++ b/src/server/BreCal/api/shipcalls.py @@ -3,6 +3,7 @@ from webargs.flaskparser import parser from marshmallow import Schema, fields from ..schemas import model from .. import impl +from ..services.auth_guard import auth_guard import logging import json @@ -12,6 +13,7 @@ bp = Blueprint('shipcalls', __name__) # TODO: verify token @bp.route('/shipcalls', methods=['get']) +@auth_guard() # no restriction by role def GetShipcalls(): if 'Authorization' in request.headers: token = request.headers.get('Authorization') @@ -24,6 +26,7 @@ def GetShipcalls(): @bp.route('/shipcalls', methods=['post']) +@auth_guard() # no restriction by role def PostShipcalls(): try: @@ -38,6 +41,7 @@ def PostShipcalls(): @bp.route('/shipcalls', methods=['put']) +@auth_guard() # no restriction by role def PutShipcalls(): try: diff --git a/src/server/BreCal/api/ships.py b/src/server/BreCal/api/ships.py index e4d7958..d465b00 100644 --- a/src/server/BreCal/api/ships.py +++ b/src/server/BreCal/api/ships.py @@ -1,10 +1,12 @@ from flask import Blueprint, request from .. import impl +from ..services.auth_guard import auth_guard import json bp = Blueprint('ships', __name__) @bp.route('/ships', methods=['get']) +@auth_guard() # no restriction by role def GetShips(): if 'Authentication' in request.headers: token = request.headers.get('Authentication') diff --git a/src/server/BreCal/api/times.py b/src/server/BreCal/api/times.py index 3df4937..2c90397 100644 --- a/src/server/BreCal/api/times.py +++ b/src/server/BreCal/api/times.py @@ -1,6 +1,7 @@ from flask import Blueprint, request from ..schemas import model from .. import impl +from ..services.auth_guard import auth_guard import json import logging @@ -8,6 +9,7 @@ bp = Blueprint('times', __name__) @bp.route('/times', methods=['get']) +@auth_guard() # no restriction by role def GetTimes(): options = {} @@ -17,6 +19,7 @@ def GetTimes(): @bp.route('/times', methods=['post']) +@auth_guard() # no restriction by role def PostTimes(): try: @@ -36,6 +39,7 @@ def PostTimes(): @bp.route('/times', methods=['put']) +@auth_guard() # no restriction by role def PutTimes(): try: @@ -51,6 +55,7 @@ def PutTimes(): @bp.route('/times', methods=['delete']) +@auth_guard() # no restriction by role def DeleteTimes(): # TODO check if I am allowd to delete this thing by deriving the participant from the bearer token diff --git a/src/server/BreCal/impl/login.py b/src/server/BreCal/impl/login.py index 38c2f17..eaa50f1 100644 --- a/src/server/BreCal/impl/login.py +++ b/src/server/BreCal/impl/login.py @@ -5,6 +5,7 @@ import bcrypt from ..schemas import model from .. import local_db +from ..services import jwt_handler def GetUser(options): @@ -15,15 +16,19 @@ def GetUser(options): commands = pydapper.using(local_db.connection_pool) data = commands.query("SELECT id, participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash, api_key FROM user WHERE user_name = ?username? OR user_email = ?username?", model=model.User, param={"username" : options["username"]}) - print(data) + # print(data) if len(data) == 1: if bcrypt.checkpw(options["password"].encode("utf-8"), bytes(data[0].password_hash, "utf-8")): - return json.dumps({ "id": data[0].id, - "participant_id": data[0].participant_id, - "first_name": data[0].first_name, - "last_name": data[0].last_name, - "user_name": data[0].user_name, - "user_phone": data[0].user_phone}), 200 + result = { + "id": data[0].id, + "participant_id": data[0].participant_id, + "first_name": data[0].first_name, + "last_name": data[0].last_name, + "user_name": data[0].user_name, + "user_phone": data[0].user_phone + } + token = jwt_handler.generate_jwt(payload=result, lifetime=60) # generate token valid 60 mins + return token, 200 if len(data) > 1: return json.dumps("credential lookup mismatch"), 500 diff --git a/src/server/BreCal/services/auth_guard.py b/src/server/BreCal/services/auth_guard.py new file mode 100644 index 0000000..9c5824d --- /dev/null +++ b/src/server/BreCal/services/auth_guard.py @@ -0,0 +1,34 @@ +import json +from flask import request +from .jwt_handler import decode_jwt + +def check_jwt(): + # get header and try to get payload + # this will throw an exception if the payload is missing, invalid or expired + token = request.headers.get('Authorization') + if not token: + raise Exception('Missing access token') + jwt = token.split('Bearer ')[1] + try: + return decode_jwt(jwt) + except Exception as e: + raise Exception(f'invalid access token: {e}') + +# magic. use this to decorate the api calls +# https://brunotatsuya.dev/blog/jwt-authentication-and-authorization-for-python-flask-rest-apis + +def auth_guard(role=None): + def wrapper(route_function): + def decorated_function(*args, **kwargs): + # Authentication gate + try: + user_data = check_jwt() + except Exception as e: + return json.dumps({"message" : f'{e}', "status": 401}), 401 + if role and role not in user_data['roles']: + return json.dumps({"message": 'Authorization required.', "status" : 403}), 403 + # get on to original route + return route_function(*args, **kwargs) + decorated_function.__name__ = route_function.__name__ + return decorated_function + return wrapper \ No newline at end of file diff --git a/src/server/BreCal/services/jwt_handler.py b/src/server/BreCal/services/jwt_handler.py new file mode 100644 index 0000000..66c0f43 --- /dev/null +++ b/src/server/BreCal/services/jwt_handler.py @@ -0,0 +1,17 @@ +import os +import jwt +import datetime +import secrets + +def create_api_key(): + return secrets.token_urlsafe(16) + +def generate_jwt(payload, lifetime=None): + if lifetime: + payload['exp'] = (datetime.datetime.now() + datetime.timedelta(minutes=lifetime)).timestamp() + return jwt.encode(payload, os.environ.get('SECRET_KEY'), algorithm="HS256") + +def decode_jwt(token): + return jwt.decode(token, os.environ.get('SECRET_KEY'), algorithms=["HS256"]) + +