diff --git a/src/server/BreCal/api/shipcalls.py b/src/server/BreCal/api/shipcalls.py index b5b8898..1f3f0c3 100644 --- a/src/server/BreCal/api/shipcalls.py +++ b/src/server/BreCal/api/shipcalls.py @@ -23,7 +23,7 @@ bp = Blueprint('shipcalls', __name__) def GetShipcalls(): try: if 'Authorization' in request.headers: - token = request.headers.get('Authorization') # see impl/login to see the token encoding, which is a JWT token. + token = request.headers.get('Authorization') # see impl/login to see the token encoding, which is a JWT token. """ from BreCal.services.jwt_handler import decode_jwt @@ -34,14 +34,14 @@ def GetShipcalls(): payload = decode_jwt(request.headers.get("Authorization").split("Bearer ")[-1]) """ payload = decode_jwt(request.headers.get("Authorization").split("Bearer ")[-1]) - options = {} + options = {} options["past_days"] = request.args.get("past_days", default=1, type=int) options["participant_id"] = payload["participant_id"] return impl.shipcalls.GetShipcalls(options) else: return create_dynamic_exception_response(ex=None, status_code=403, message="not authenticated") - + except Exception as ex: return create_dynamic_exception_response(ex=ex, status_code=400) @@ -62,10 +62,10 @@ def PostShipcalls(): # validate the posted shipcall data & the user's authority InputValidationShipcall.evaluate_post_data(user_data, loadedModel, content) return impl.shipcalls.PostShipcalls(loadedModel) - + except ValidationError as ex: return create_validation_error_response(ex=ex, status_code=400) - + except Exception as ex: logging.error(traceback.format_exc()) return create_dynamic_exception_response(ex=ex, status_code=400, message="bad format") @@ -77,23 +77,26 @@ def PutShipcalls(): try: verify_if_request_is_json(request) - + content = request.get_json(force=True) loadedModel = model.ShipcallSchema().load(data=content, many=False, partial=True) # read the user data from the JWT token (set when login is performed) user_data = check_jwt() + if not InputValidationShipcall.exists_shipcall_by_id(loadedModel.get("id")): + return create_dynamic_exception_response(ex=None, status_code=404, message="no shipcall found with the provided id") + # validate the PUT shipcall data and the user's authority InputValidationShipcall.evaluate_put_data(user_data, loadedModel, content) return impl.shipcalls.PutShipcalls(loadedModel) - + except ValidationError as ex: return create_validation_error_response(ex=ex, status_code=400) except werkzeug.exceptions.Forbidden as ex: return create_werkzeug_error_response(ex=ex, status_code=403) - + except Exception as ex: logging.error(traceback.format_exc()) return create_dynamic_exception_response(ex=None, status_code=400, message="bad format") diff --git a/src/server/BreCal/api/ships.py b/src/server/BreCal/api/ships.py index 482474b..e8e2626 100644 --- a/src/server/BreCal/api/ships.py +++ b/src/server/BreCal/api/ships.py @@ -23,7 +23,7 @@ def GetShips(): return impl.ships.GetShips(token) else: return create_dynamic_exception_response(ex=None, status_code=403, message="not authenticated") - + except Exception as ex: return create_dynamic_exception_response(ex=ex, status_code=400) @@ -43,7 +43,7 @@ def PostShip(): is_bsmd = check_if_user_is_bsmd_type(user_data) if not is_bsmd: raise ValidationError({"participant_type":f"current user does not belong to BSMD. Cannot post shipcalls. Found user data: {user_data}"}) - + content = request.get_json(force=True) loadedModel = model.ShipSchema().load(data=content, many=False, partial=True) @@ -53,7 +53,7 @@ def PostShip(): except ValidationError as ex: return create_validation_error_response(ex=ex, status_code=400) - + except Exception as ex: return create_dynamic_exception_response(ex=ex, status_code=400, message=None) @@ -71,13 +71,16 @@ def PutShip(): content = request.get_json(force=True) loadedModel = model.ShipSchema().load(data=content, many=False, partial=True, unknown=EXCLUDE) + if not InputValidationShip.exists_ship_by_dict(model=loadedModel): + return create_dynamic_exception_response(ex=None, status_code=404, message="no ship found with the provided id") + # validate the request data & user permissions InputValidationShip.evaluate_put_data(user_data, loadedModel, content) return impl.ships.PutShip(loadedModel) - + except ValidationError as ex: return create_validation_error_response(ex=ex, status_code=400) - + except Exception as ex: return create_dynamic_exception_response(ex=ex, status_code=400) @@ -88,10 +91,10 @@ def DeleteShip(): try: verify_if_request_is_json(request) - + # read the user data from the JWT token (set when login is performed) user_data = check_jwt() - + if 'id' in request.args: options = {} options["id"] = request.args.get("id") @@ -100,12 +103,16 @@ def DeleteShip(): # validate the request data & user permissions ship_id = request.args.get("id") + + if not InputValidationShip.exists_ship_by_id(id=ship_id): + return create_dynamic_exception_response(ex=None, status_code=404, message="no ship found with the provided id") + InputValidationShip.evaluate_delete_data(user_data, ship_id) return impl.ships.DeleteShip(options) - + except ValidationError as ex: return create_validation_error_response(ex=ex, status_code=400) - + except Exception as ex: return create_dynamic_exception_response(ex=ex, status_code=400) diff --git a/src/server/BreCal/api/times.py b/src/server/BreCal/api/times.py index ff8e17e..23537bd 100644 --- a/src/server/BreCal/api/times.py +++ b/src/server/BreCal/api/times.py @@ -20,7 +20,7 @@ def GetTimes(): options = {} options["shipcall_id"] = request.args.get("shipcall_id") return impl.times.GetTimes(options) - + except Exception as ex: return create_dynamic_exception_response(ex=ex, status_code=400) @@ -60,13 +60,16 @@ def PutTimes(): try: verify_if_request_is_json(request) - + content = request.get_json(force=True) loadedModel = model.TimesSchema().load(data=content, many=False, partial=True) # read the user data from the JWT token (set when login is performed) user_data = check_jwt() - + + if not InputValidationTimes.exists_times_by_id(loadedModel.get("id")): + return create_dynamic_exception_response(ex=None, status_code=404, message="no times found with the provided id") + # validate the request InputValidationTimes.evaluate_put_data(user_data, loadedModel, content) return impl.times.PutTimes(loadedModel) @@ -91,13 +94,16 @@ def DeleteTimes(): # read the user data from the JWT token (set when login is performed) user_data = check_jwt() + if not InputValidationTimes.exists_times_by_id(options["id"]): + return create_dynamic_exception_response(ex=None, status_code=404, message="no times found with the provided id") + # validate the request InputValidationTimes.evaluate_delete_data(user_data, times_id = request.args.get("id")) return impl.times.DeleteTimes(options) else: return create_dynamic_exception_response(ex=None, status_code=400, message="Times delete missing argument: id") - + except ValidationError as ex: return create_validation_error_response(ex=ex, status_code=400) diff --git a/src/server/BreCal/database/sql_utils.py b/src/server/BreCal/database/sql_utils.py index d4aefef..b329436 100644 --- a/src/server/BreCal/database/sql_utils.py +++ b/src/server/BreCal/database/sql_utils.py @@ -4,19 +4,27 @@ import datetime def get_user_data_for_id(user_id:int, expiration_time:int=90): """debugging function, which is useful to pull user_data from the database, which may be used to create stub data and unit tests""" query = "SELECT * FROM user where id = ?id?" - pdata = execute_sql_query_standalone(query=query, param={"id":user_id}) - pdata = pdata[0] if len(pdata)>0 else None + pdata = execute_sql_query_standalone(query=query, param={"id":user_id}, command_type="single_or_none") assert pdata is not None, f"could not find user with id {user_id}" user_data = {k:v for k,v in pdata.items() if k in ['id','participant_id','first_name','last_name','user_name','user_phone','user_email']} user_data["exp"] = (datetime.datetime.now()+datetime.timedelta(minutes=expiration_time)).timestamp() return user_data - def get_times_data_for_id(times_id:int): """helper function to load previous times data from the database""" query = "SELECT * FROM times where id = ?id?" - pdata = execute_sql_query_standalone(query=query, param={"id":times_id}) - pdata = pdata[0] if len(pdata)>0 else None - assert pdata is not None, f"could not find times with id {times_id}" + pdata = execute_sql_query_standalone(query=query, param={"id":times_id}, command_type="single_or_none") + return pdata + +def get_ship_data_for_id(ship_id:int): + """helper function to load previous ship data from the database""" + query = "SELECT * FROM ship where id = ?id?" + pdata = execute_sql_query_standalone(query=query, param={"id":ship_id}, command_type="single_or_none") + return pdata + +def get_shipcall_data_for_id(shipcall_id:int): + """helper function to load previous shipcall data from the database""" + query = "SELECT * FROM shipcall where id = ?id?" + pdata = execute_sql_query_standalone(query=query, param={"id":shipcall_id}, command_type="single_or_none") return pdata \ No newline at end of file diff --git a/src/server/BreCal/validators/input_validation_ship.py b/src/server/BreCal/validators/input_validation_ship.py index ce1c5e4..c6ad45b 100644 --- a/src/server/BreCal/validators/input_validation_ship.py +++ b/src/server/BreCal/validators/input_validation_ship.py @@ -6,8 +6,9 @@ from marshmallow import ValidationError from string import ascii_letters, digits from BreCal.schemas.model import Ship, Shipcall, Berth, User, Participant, ShipcallType -from BreCal.database.sql_handler import execute_sql_query_standalone +from BreCal.database.sql_handler import execute_sql_query_standalone from BreCal.database.sql_queries import SQLQuery +from BreCal.database.sql_utils import get_ship_data_for_id from BreCal.impl.participant import GetParticipant from BreCal.impl.ships import GetShips from BreCal.impl.berths import GetBerths @@ -17,6 +18,7 @@ from BreCal.validators.input_validation_utils import check_if_user_is_bsmd_type, from BreCal.database.sql_handler import execute_sql_query_standalone from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag from BreCal.validators.validation_base_utils import check_if_string_has_special_characters + import werkzeug class InputValidationShip(): @@ -27,23 +29,34 @@ class InputValidationShip(): Example: InputValidationShip.evaluate(user_data, loadedModel, content) - When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues. + When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues. """ def __init__(self) -> None: pass + @staticmethod + def exists_ship_by_dict(model:dict): + ship_id = model.get("id") + if(ship_id is not None): + return get_ship_data_for_id(ship_id) is not None + return False + + @staticmethod + def exists_ship_by_id(id:int): + return get_ship_data_for_id(id) is not None + @staticmethod def evaluate_post_data(user_data:dict, loadedModel:dict, content:dict): # 1.) Only users of type BSMD are allowed to POST InputValidationShip.check_user_is_bsmd_type(user_data) - # 2.) The ship IMOs are used as matching keys. They must be unique in the database. + # 2.) The ship IMOs are used as matching keys. They must be unique in the database. InputValidationShip.check_ship_imo_already_exists(loadedModel) # 3.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema) InputValidationShip.optionally_evaluate_bollard_pull_value(content) return - + @staticmethod def evaluate_put_data(user_data:dict, loadedModel:dict, content:dict): # 1.) Only users of type BSMD are allowed to PUT @@ -58,20 +71,20 @@ class InputValidationShip(): # 4.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema) InputValidationShip.optionally_evaluate_bollard_pull_value(content) return - + @staticmethod def evaluate_delete_data(user_data:dict, ship_id:typing.Optional[int]): if ship_id is None: raise ValidationError({"id":f"The ship id must be provided."}) ship_id = int(ship_id) - + # 1.) Only users of type BSMD are allowed to PUT InputValidationShip.check_user_is_bsmd_type(user_data) # 2.) The dataset entry may not be deleted already InputValidationShip.check_if_entry_is_already_deleted(ship_id) return - + @staticmethod def optionally_evaluate_bollard_pull_value(content:dict): bollard_pull = content.get("bollard_pull",None) @@ -89,7 +102,7 @@ class InputValidationShip(): is_bsmd = check_if_user_is_bsmd_type(user_data) if not is_bsmd: raise ValidationError({"participant_type":f"current user does not belong to BSMD. Cannot post, put or delete ships. Found user data: {user_data}"}) - + @staticmethod def check_ship_imo_already_exists(loadedModel:dict): # get the ships, convert them to a list of JSON dictionaries @@ -104,28 +117,28 @@ class InputValidationShip(): if imo_already_exists: raise ValidationError({"imo":f"the provided ship IMO {loadedModel.get('imo')} already exists. A ship may only be added, if there is no other ship with the same IMO number."}) return - + @staticmethod def put_content_may_not_contain_imo_number(content:dict): # IMO is a required field, so it will never be None outside of tests. If so, there is no violation put_data_ship_imo = content.get("imo",None) if put_data_ship_imo is None: return - + # grab the ship by its ID and compare, whether the IMO is unchanged ship = execute_sql_query_standalone(SQLQuery.get_ship_by_id(), param={"id":content.get("id")}, command_type="single", model=Ship) if put_data_ship_imo != ship.imo: raise ValidationError({"imo":f"The IMO number field may not be changed since it serves the purpose of a primary (matching) key."}) return - + @staticmethod def content_contains_ship_id(content:dict): put_data_ship_id = content.get('id',None) if put_data_ship_id is None: raise ValidationError({"id":f"The id field is required."}) return - + @staticmethod def check_if_entry_is_already_deleted(ship_id:typing.Optional[int]): """ @@ -147,4 +160,4 @@ class InputValidationShip(): return - + diff --git a/src/server/BreCal/validators/input_validation_shipcall.py b/src/server/BreCal/validators/input_validation_shipcall.py index 52e380f..3883437 100644 --- a/src/server/BreCal/validators/input_validation_shipcall.py +++ b/src/server/BreCal/validators/input_validation_shipcall.py @@ -11,6 +11,7 @@ from BreCal.impl.ships import GetShips from BreCal.impl.berths import GetBerths from BreCal.database.enums import ParticipantType, ParticipantFlag +from BreCal.database.sql_utils import get_shipcall_data_for_id from BreCal.validators.input_validation_utils import check_if_user_is_bsmd_type, check_if_ship_id_is_valid, check_if_berth_id_is_valid, check_if_participant_ids_are_valid, check_if_participant_ids_and_types_are_valid, get_shipcall_id_dictionary, get_participant_type_from_user_data from BreCal.database.sql_handler import get_assigned_participant_of_type from BreCal.database.sql_handler import execute_sql_query_standalone @@ -98,6 +99,10 @@ class InputValidationShipcall(): InputValidationShipcall.check_shipcall_is_canceled(loadedModel, content) return + @staticmethod + def exists_shipcall_by_id(id:int): + return get_shipcall_data_for_id(id) is not None + @staticmethod def check_shipcall_values(loadedModel:dict, content:dict, forbidden_keys:list=["evaluation", "evaluation_message"], is_put_data:bool=False): """ @@ -127,7 +132,7 @@ class InputValidationShipcall(): if is_put_data: # the type of a shipcall may not be changed. It can only be set with the initial POST-request. InputValidationShipcall.check_shipcall_type_is_unchanged(loadedModel) - + InputValidationShipcall.check_times_are_in_future(loadedModel, content) # some arguments must not be provided @@ -365,7 +370,7 @@ class InputValidationShipcall(): """ if (eta is None) and (etd is None): return - + time_in_a_year = time_now.replace(time_now.year + 1) if type_ is None: @@ -419,7 +424,7 @@ class InputValidationShipcall(): raise ValidationError({"eta":f"'eta' is more than a year in the future. ETA: {eta}."}) if etd > time_in_a_year: raise ValidationError({"etd":f"'etd' is more than a year in the future. ETD: {etd}."}) - + return @staticmethod @@ -442,7 +447,7 @@ class InputValidationShipcall(): if (tidal_window_to is not None) and (tidal_window_from is not None): if tidal_window_to < tidal_window_from: raise ValidationError({"tidal_window_to_or_tidal_window_from":f"'tidal_window_to' must take place after 'tidal_window_from'. Incorrect datetime provided. Found 'tidal_window_to': {tidal_window_to}, 'tidal_window_from': {tidal_window_to}."}) - + if (tidal_window_to is not None and tidal_window_from is None) or (tidal_window_to is None and tidal_window_from is not None): raise ValidationError({"tidal_window_to_or_tidal_window_from":f"'tidal_window_to' and 'tidal_window_from' must both be provided."}) diff --git a/src/server/BreCal/validators/input_validation_times.py b/src/server/BreCal/validators/input_validation_times.py index 9db0a2c..97eaf51 100644 --- a/src/server/BreCal/validators/input_validation_times.py +++ b/src/server/BreCal/validators/input_validation_times.py @@ -128,6 +128,10 @@ class InputValidationTimes(): InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id) return + @staticmethod + def exists_times_by_id(id:int): + return get_times_data_for_id(id) is not None + @staticmethod def check_if_entry_is_already_deleted(times_id:int): """