Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions backend/src/controller/aws_apig_event_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

from aws_lambda_typing.events import APIGatewayProxyEventV1

from controller.constants import SUPPLIER_SYSTEM_HEADER_NAME
from models.errors import UnauthorizedError
from controller.constants import E_TAG_HEADER_NAME, SUPPLIER_SYSTEM_HEADER_NAME
from models.errors import ResourceVersionNotProvided, UnauthorizedError
from utils import dict_utils


Expand All @@ -14,11 +14,22 @@ def get_path_parameter(event: APIGatewayProxyEventV1, param_name: str) -> str:


def get_supplier_system_header(event: APIGatewayProxyEventV1) -> str:
"""Retrieves the supplier system header from the API Gateway event"""
"""Retrieves the supplier system header from the API Gateway event. Raises an Unauthorized error if not present."""
supplier_system: Optional[str] = dict_utils.get_field(dict(event), "headers", SUPPLIER_SYSTEM_HEADER_NAME)

if supplier_system is None:
# SupplierSystem header must be provided for looking up permissions
raise UnauthorizedError()

return supplier_system


def get_resource_version_header(event: APIGatewayProxyEventV1) -> str:
"""Retrieves the resource version header from the API Gateway event. Raises a ResourceVersionNotProvided if not
present."""
resource_version_header: Optional[str] = dict_utils.get_field(dict(event), "headers", E_TAG_HEADER_NAME)

if resource_version_header is None:
raise ResourceVersionNotProvided(resource_type="Immunization")

return resource_version_header
10 changes: 10 additions & 0 deletions backend/src/controller/fhir_api_exception_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@
Code,
CustomValidationError,
IdentifierDuplicationError,
InconsistentIdentifierError,
InconsistentIdError,
InconsistentResourceVersion,
InvalidImmunizationId,
InvalidJsonError,
InvalidResourceVersion,
ResourceNotFoundError,
ResourceVersionNotProvided,
Severity,
UnauthorizedError,
UnauthorizedVaxError,
Expand All @@ -22,9 +27,14 @@
)

_CUSTOM_EXCEPTION_TO_STATUS_MAP: dict[Type[Exception], int] = {
InconsistentResourceVersion: 400,
InconsistentIdentifierError: 400,
InconsistentIdError: 400,
InvalidImmunizationId: 400,
InvalidJsonError: 400,
InvalidResourceVersion: 400,
CustomValidationError: 400,
ResourceVersionNotProvided: 400,
UnauthorizedError: 403,
UnauthorizedVaxError: 403,
ResourceNotFoundError: 404,
Expand Down
203 changes: 25 additions & 178 deletions backend/src/controller/fhir_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,22 @@

from controller.aws_apig_event_utils import (
get_path_parameter,
get_resource_version_header,
get_supplier_system_header,
)
from controller.aws_apig_response_utils import create_response
from controller.constants import E_TAG_HEADER_NAME
from controller.fhir_api_exception_handler import fhir_api_exception_handler
from models.errors import (
Code,
IdentifierDuplicationError,
InconsistentIdError,
InvalidImmunizationId,
InvalidJsonError,
InvalidResourceVersion,
ParameterException,
Severity,
UnauthorizedError,
UnauthorizedVaxError,
ValidationError,
create_operation_outcome,
)
from models.utils.generic_utils import check_keys_in_sources
Expand Down Expand Up @@ -124,189 +125,31 @@ def create_immunization(self, aws_event: APIGatewayProxyEventV1) -> dict:
headers={"Location": f"{self._API_SERVICE_URL}/Immunization/{created_resource_id}", "E-Tag": "1"},
)

def update_immunization(self, aws_event):
try:
if aws_event.get("headers"):
imms_id = aws_event["pathParameters"]["id"]
else:
raise UnauthorizedError()
except UnauthorizedError as unauthorized:
return create_response(403, unauthorized.to_operation_outcome())

supplier_system = self._identify_supplier_system(aws_event)
@fhir_api_exception_handler
def update_immunization(self, aws_event: APIGatewayProxyEventV1) -> dict:
imms_id = get_path_parameter(aws_event, "id")
supplier_system = get_supplier_system_header(aws_event)
resource_version = get_resource_version_header(aws_event)

# Refactor to raise InvalidImmunizationId when working on VED-747
if not self._is_valid_immunization_id(imms_id):
return create_response(
400,
json.dumps(
create_operation_outcome(
resource_id=str(uuid.uuid4()),
severity=Severity.error,
code=Code.invalid,
diagnostics="Validation errors: the provided event ID is either missing or not in the expected "
"format.",
)
),
)
raise InvalidImmunizationId()

# Validate the body of the request - start
try:
imms = json.loads(aws_event["body"], parse_float=Decimal)
# Validate the imms id in the path params and body of request - start
if imms.get("id") != imms_id:
exp_error = create_operation_outcome(
resource_id=str(uuid.uuid4()),
severity=Severity.error,
code=Code.invariant,
diagnostics=(
f"Validation errors: The provided immunization id:{imms_id} doesn't match with the content of "
"the request body"
),
)
return create_response(400, json.dumps(exp_error))
# Validate the imms id in the path params and body of request - end
except JSONDecodeError as e:
return self._create_bad_request(f"Request's body contains malformed JSON: {e}")
except Exception as e:
return self._create_bad_request(f"Request's body contains string: {e}")
# Validate the body of the request - end
if not self._is_valid_resource_version(resource_version):
raise InvalidResourceVersion(resource_version=resource_version)

# Validate if the imms resource does not exist - start
try:
existing_record = self.fhir_service.get_immunization_by_id_all(imms_id, imms)
if not existing_record:
exp_error = create_operation_outcome(
resource_id=str(uuid.uuid4()),
severity=Severity.error,
code=Code.not_found,
diagnostics=(
f"Validation errors: The requested immunization resource with id:{imms_id} was not found."
),
)
return create_response(404, json.dumps(exp_error))

if "diagnostics" in existing_record:
exp_error = create_operation_outcome(
resource_id=str(uuid.uuid4()),
severity=Severity.error,
code=Code.invariant,
diagnostics=existing_record["diagnostics"],
)
return create_response(400, json.dumps(exp_error))
except ValidationError as error:
return create_response(400, error.to_operation_outcome())
# Validate if the imms resource does not exist - end
immunization = json.loads(aws_event["body"], parse_float=Decimal)
except JSONDecodeError as e:
# Consider moving the start of the message into a const
raise InvalidJsonError(message=str(f"Request's body contains malformed JSON: {e}"))

existing_resource_version = int(existing_record["Version"])
existing_resource_vacc_type = existing_record["VaccineType"]
if immunization.get("id") != imms_id:
raise InconsistentIdError(imms_id=imms_id)

try:
# Validate if the imms resource to be updated is a logically deleted resource - start
if existing_record["DeletedAt"]:
outcome, resource, updated_version = self.fhir_service.reinstate_immunization(
imms_id,
imms,
existing_resource_version,
existing_resource_vacc_type,
supplier_system,
)
# Validate if the imms resource to be updated is a logically deleted resource-end
else:
# Validate if imms resource version is part of the request - start
if "E-Tag" not in aws_event["headers"]:
exp_error = create_operation_outcome(
resource_id=str(uuid.uuid4()),
severity=Severity.error,
code=Code.invariant,
diagnostics=(
"Validation errors: Immunization resource version not specified in the request headers"
),
)
return create_response(400, json.dumps(exp_error))
# Validate if imms resource version is part of the request - end

# Validate the imms resource version provided in the request headers - start
try:
resource_version_header = int(aws_event["headers"]["E-Tag"])
except (TypeError, ValueError):
resource_version = aws_event["headers"]["E-Tag"]
exp_error = create_operation_outcome(
resource_id=str(uuid.uuid4()),
severity=Severity.error,
code=Code.invariant,
diagnostics=(
f"Validation errors: Immunization resource version:{resource_version} in the request "
"headers is invalid."
),
)
return create_response(400, json.dumps(exp_error))
# Validate the imms resource version provided in the request headers - end

# Validate if resource version has changed since the last retrieve - start
if existing_resource_version > resource_version_header:
exp_error = create_operation_outcome(
resource_id=str(uuid.uuid4()),
severity=Severity.error,
code=Code.invariant,
diagnostics=(
f"Validation errors: The requested immunization resource {imms_id} has changed since the "
"last retrieve."
),
)
return create_response(400, json.dumps(exp_error))
if existing_resource_version < resource_version_header:
exp_error = create_operation_outcome(
resource_id=str(uuid.uuid4()),
severity=Severity.error,
code=Code.invariant,
diagnostics=(
f"Validation errors: The requested immunization resource {imms_id} version is inconsistent "
"with the existing version."
),
)
return create_response(400, json.dumps(exp_error))
# Validate if resource version has changed since the last retrieve - end

# Check if the record is reinstated record - start
if existing_record["Reinstated"] is True:
outcome, resource, updated_version = self.fhir_service.update_reinstated_immunization(
imms_id,
imms,
existing_resource_version,
existing_resource_vacc_type,
supplier_system,
)
else:
outcome, resource, updated_version = self.fhir_service.update_immunization(
imms_id,
imms,
existing_resource_version,
existing_resource_vacc_type,
supplier_system,
)

# Check if the record is reinstated record - end

# Check for errors returned on update
if "diagnostics" in resource:
exp_error = create_operation_outcome(
resource_id=str(uuid.uuid4()),
severity=Severity.error,
code=Code.invariant,
diagnostics=resource["diagnostics"],
)
return create_response(400, json.dumps(exp_error))
if outcome:
return create_response(
200, None, {"E-Tag": updated_version}
) # include e-tag here, is it not included in the response resource
except ValidationError as error:
return create_response(400, error.to_operation_outcome())
except IdentifierDuplicationError as duplicate:
return create_response(422, duplicate.to_operation_outcome())
except UnauthorizedVaxError as unauthorized:
return create_response(403, unauthorized.to_operation_outcome())
updated_resource_version = self.fhir_service.update_immunization(
imms_id, immunization, supplier_system, int(resource_version)
)
return create_response(200, None, {E_TAG_HEADER_NAME: updated_resource_version})

@fhir_api_exception_handler
def delete_immunization(self, aws_event: APIGatewayProxyEventV1) -> dict:
Expand Down Expand Up @@ -385,6 +228,10 @@ def _is_valid_immunization_id(self, immunization_id: str) -> bool:
"""Validates if the given unique Immunization ID is valid."""
return False if not re.match(self._IMMUNIZATION_ID_PATTERN, immunization_id) else True

@staticmethod
def _is_valid_resource_version(resource_version: str) -> bool:
return resource_version.isdigit() and int(resource_version) > 0
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testers - minor change. Previously we permitted 0 and negative numbers. We will now catch such an error earlier on in the flow and get an error stating the the resource version is invalid.

Previously, this would have been accepted but then the update would fail on the old resource vs. new resource version check, as a resource in the database would only have a positive int as the resource version due to the constraints during both create and update operations.


def _validate_identifier_system(self, _id: str, _elements: str) -> Optional[dict]:
if not _id:
return create_operation_outcome(
Expand Down
2 changes: 2 additions & 0 deletions backend/src/models/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,5 @@ class Constants:
SUPPLIER_PERMISSIONS_KEY = "supplier_permissions"
VACCINE_TYPE_TO_DISEASES_HASH_KEY = "vacc_to_diseases"
DISEASES_TO_VACCINE_TYPE_HASH_KEY = "diseases_to_vacc"

REINSTATED_RECORD_STATUS = "reinstated"
Loading