Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion infrastructure/modules/lambda/lambda.tf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ resource "aws_lambda_function" "eligibility_signposting_lambda" {
ENV = var.environment
LOG_LEVEL = var.log_level
ENABLE_XRAY_PATCHING = var.enable_xray_patching
API_DOMAIN_NAME = var.api_domain_name
}
}

Expand All @@ -36,7 +37,9 @@ resource "aws_lambda_function" "eligibility_signposting_lambda" {
}

layers = compact([
var.environment == "prod" ? "arn:aws:lambda:${var.region}:580247275435:layer:LambdaInsightsExtension:${var.lambda_insights_extension_version}" : null
var.environment == "prod" ?
"arn:aws:lambda:${var.region}:580247275435:layer:LambdaInsightsExtension:${var.lambda_insights_extension_version}"
: null
])

tracing_config {
Expand Down
5 changes: 5 additions & 0 deletions infrastructure/modules/lambda/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,8 @@ variable "lambda_insights_extension_version" {
description = "version number of LambdaInsightsExtension"
type = number
}

variable "api_domain_name" {
description = "api domain name - env variable for status endpoint response"
type = string
}
14 changes: 7 additions & 7 deletions infrastructure/stacks/api-layer/api_gateway.tf
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ module "eligibility_signposting_api_gateway" {
tags = local.tags
}

resource "aws_api_gateway_resource" "_status" {
rest_api_id = module.eligibility_signposting_api_gateway.rest_api_id
parent_id = module.eligibility_signposting_api_gateway.root_resource_id
path_part = "_status"
}

resource "aws_api_gateway_resource" "patient_check" {
rest_api_id = module.eligibility_signposting_api_gateway.rest_api_id
parent_id = module.eligibility_signposting_api_gateway.root_resource_id
Expand All @@ -26,6 +20,12 @@ resource "aws_api_gateway_resource" "patient" {
path_part = "{id}"
}

resource "aws_api_gateway_resource" "patient_check_status" {
rest_api_id = module.eligibility_signposting_api_gateway.rest_api_id
parent_id = aws_api_gateway_resource.patient_check.id
path_part = "_status"
}

# deployment

resource "aws_api_gateway_deployment" "eligibility_signposting_api" {
Expand All @@ -34,7 +34,7 @@ resource "aws_api_gateway_deployment" "eligibility_signposting_api" {
triggers = {
redeployment = sha1(jsonencode([
aws_api_gateway_integration.get_patient_check.id,
aws_api_gateway_integration._status.id,
aws_api_gateway_integration.get_patient_check_status.id
]))
}

Expand Down
66 changes: 0 additions & 66 deletions infrastructure/stacks/api-layer/healthcheck_status.tf

This file was deleted.

3 changes: 2 additions & 1 deletion infrastructure/stacks/api-layer/lambda.tf
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module "eligibility_signposting_lambda_function" {
environment = var.environment
runtime = "python3.13"
lambda_func_name = "${terraform.workspace == "default" ? "" : "${terraform.workspace}-"}eligibility_signposting_api"
security_group_ids = [data.aws_security_group.main_sg.id]
security_group_ids = [data.aws_security_group.main_sg.id]
Copy link
Collaborator

Choose a reason for hiding this comment

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

random space removal?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes,
alt + ctrl + L is the culprit.

vpc_intra_subnets = [for v in data.aws_subnet.private_subnets : v.id]
file_name = "../../../dist/lambda.zip"
handler = "eligibility_signposting_api.app.lambda_handler"
Expand All @@ -30,4 +30,5 @@ module "eligibility_signposting_lambda_function" {
enable_xray_patching = "true"
stack_name = local.stack_name
provisioned_concurrency_count = 5
api_domain_name = local.api_domain_name
}
28 changes: 28 additions & 0 deletions infrastructure/stacks/api-layer/patient_check.tf
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,34 @@ resource "aws_api_gateway_integration" "get_patient_check" {
]
}

resource "aws_api_gateway_method" "get_patient_check_status" {
#checkov:skip=CKV_AWS_59: API is secured via Apigee proxy with mTLS, API keys are not used
#checkov:skip=CKV2_AWS_53: No request parameters to validate for static healthcheck endpoint
rest_api_id = module.eligibility_signposting_api_gateway.rest_api_id
resource_id = aws_api_gateway_resource.patient_check_status.id
http_method = "GET"
authorization = "NONE"
api_key_required = false

depends_on = [
aws_api_gateway_resource.patient_check_status,
aws_api_gateway_resource.patient_check,
]
}

resource "aws_api_gateway_integration" "get_patient_check_status" {
rest_api_id = module.eligibility_signposting_api_gateway.rest_api_id
resource_id = aws_api_gateway_resource.patient_check_status.id
http_method = aws_api_gateway_method.get_patient_check_status.http_method
integration_http_method = "POST" # Needed for lambda proxy integration
type = "AWS_PROXY"
uri = module.eligibility_signposting_lambda_function.aws_lambda_invoke_arn

depends_on = [
aws_api_gateway_method.get_patient_check_status
]
}

resource "aws_lambda_permission" "get_patient_check" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
Expand Down
5 changes: 2 additions & 3 deletions src/eligibility_signposting_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from eligibility_signposting_api import audit, repos, services
from eligibility_signposting_api.common.cache_manager import FLASK_APP_CACHE_KEY, cache_manager
from eligibility_signposting_api.common.error_handler import handle_exception
from eligibility_signposting_api.common.request_validator import validate_request_params
from eligibility_signposting_api.config.config import config
from eligibility_signposting_api.config.contants import URL_PREFIX
from eligibility_signposting_api.logging.logs_helper import log_request_ids_from_headers
from eligibility_signposting_api.logging.logs_manager import add_lambda_request_id_to_logger, init_logging
from eligibility_signposting_api.logging.tracing_helper import tracing_setup
Expand Down Expand Up @@ -49,7 +49,6 @@ def get_or_create_app() -> Flask:
@add_lambda_request_id_to_logger()
@tracing_setup()
@log_request_ids_from_headers()
@validate_request_params()
def lambda_handler(event: LambdaEvent, context: LambdaContext) -> dict[str, Any]: # pragma: no cover
"""Run the Flask app as an AWS Lambda."""
app = get_or_create_app()
Expand All @@ -64,7 +63,7 @@ def create_app() -> Flask:
logger.info("app created")

# Register views & error handler
app.register_blueprint(eligibility_blueprint, url_prefix="/patient-check")
app.register_blueprint(eligibility_blueprint, url_prefix=f"/{URL_PREFIX}")
app.register_error_handler(Exception, handle_exception)

# Set up dependency injection using wireup
Expand Down
14 changes: 5 additions & 9 deletions src/eligibility_signposting_api/common/api_error_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
from datetime import UTC, datetime
from enum import Enum
from http import HTTPStatus
from typing import Any

from fhir.resources.operationoutcome import OperationOutcome, OperationOutcomeIssue
from flask import make_response
from flask.typing import ResponseReturnValue

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -65,7 +66,7 @@ def build_operation_outcome_issue(self, diagnostics: str, location: list[str] |
details=details,
) # pyright: ignore[reportCallIssue]

def generate_response(self, diagnostics: str, location_param: str | None = None) -> dict[str, Any]:
def generate_response(self, diagnostics: str, location_param: str | None = None) -> ResponseReturnValue:
issue_location = [f"parameters/{location_param}"] if location_param else None

problem = OperationOutcome(
Expand All @@ -75,16 +76,11 @@ def generate_response(self, diagnostics: str, location_param: str | None = None)
) # pyright: ignore[reportCallIssue]

response_body = json.dumps(problem.model_dump(by_alias=True, mode="json"))

return {
"statusCode": self.status_code,
"headers": {"Content-Type": "application/fhir+json"},
"body": response_body,
}
return make_response(response_body, self.status_code, {"Content-Type": "application/fhir+json"})

def log_and_generate_response(
self, log_message: str, diagnostics: str, location_param: str | None = None
) -> dict[str, Any]:
) -> ResponseReturnValue:
logger.error(log_message)
return self.generate_response(diagnostics, location_param)

Expand Down
6 changes: 2 additions & 4 deletions src/eligibility_signposting_api/common/error_handler.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging
import traceback

from flask import make_response
from flask.typing import ResponseReturnValue
from werkzeug.exceptions import HTTPException

Expand All @@ -11,7 +10,7 @@
logger = logging.getLogger(__name__)


def handle_exception(e: Exception) -> ResponseReturnValue | HTTPException:
def handle_exception(e: Exception) -> ResponseReturnValue:
if isinstance(e, HTTPException):
return e

Expand All @@ -25,7 +24,6 @@ def handle_exception(e: Exception) -> ResponseReturnValue | HTTPException:
logger.exception("Unexpected Exception", exc_info=e)
log_msg = f"An unexpected error occurred: {full_traceback}"

response = INTERNAL_SERVER_ERROR.log_and_generate_response(
return INTERNAL_SERVER_ERROR.log_and_generate_response(
log_message=log_msg, diagnostics="An unexpected error occurred."
)
return make_response(response.get("body"), response.get("statusCode"), response.get("headers"))
28 changes: 14 additions & 14 deletions src/eligibility_signposting_api/common/request_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import re
from collections.abc import Callable
from functools import wraps
from typing import Any

from mangum.types import LambdaContext, LambdaEvent
from flask import request
from flask.typing import ResponseReturnValue

from eligibility_signposting_api.common.api_error_response import (
INVALID_CATEGORY_ERROR,
Expand All @@ -21,7 +21,7 @@
include_actions_pattern = re.compile(r"^\s*([YN])\s*$", re.IGNORECASE)


def validate_query_params(query_params: dict[str, str]) -> tuple[bool, dict[str, Any] | None]:
def validate_query_params(query_params: dict[str, str]) -> tuple[bool, ResponseReturnValue | None]:
conditions = query_params.get("conditions", "ALL").split(",")
for condition in conditions:
search = re.search(condition_pattern, condition)
Expand All @@ -39,7 +39,7 @@ def validate_query_params(query_params: dict[str, str]) -> tuple[bool, dict[str,
return True, None


def validate_nhs_number(path_nhs: str, header_nhs: str) -> bool:
def validate_nhs_number(path_nhs: str | None, header_nhs: str | None) -> bool:
logger.info("NHS numbers from the request", extra={"header_nhs": header_nhs, "path_nhs": path_nhs})

if not header_nhs or not path_nhs:
Expand All @@ -55,28 +55,28 @@ def validate_nhs_number(path_nhs: str, header_nhs: str) -> bool:
def validate_request_params() -> Callable:
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(event: LambdaEvent, context: LambdaContext) -> dict[str, Any] | None:
path_nhs_no = event.get("pathParameters", {}).get("id")
header_nhs_no = event.get("headers", {}).get(NHS_NUMBER_HEADER)
def wrapper(*args, **kwargs) -> ResponseReturnValue: # noqa:ANN002,ANN003
path_nhs_number = str(kwargs.get("nhs_number"))
header_nhs_no = str(request.headers.get(NHS_NUMBER_HEADER))

if not validate_nhs_number(path_nhs_no, header_nhs_no):
if not validate_nhs_number(path_nhs_number, header_nhs_no):
message = "You are not authorised to request information for the supplied NHS Number"
return NHS_NUMBER_MISMATCH_ERROR.log_and_generate_response(log_message=message, diagnostics=message)

query_params = event.get("queryStringParameters")
query_params = request.args
if query_params:
is_valid, problem = validate_query_params(query_params)
if not is_valid:
if not is_valid and problem is not None:
return problem

return func(event, context)
return func(*args, **kwargs)

return wrapper

return decorator


def get_include_actions_error_response(include_actions: str) -> dict[str, Any]:
def get_include_actions_error_response(include_actions: str) -> ResponseReturnValue:
diagnostics = f"{include_actions} is not a value that is supported by the API"
return INVALID_INCLUDE_ACTIONS_ERROR.log_and_generate_response(
log_message=f"Invalid include actions query param: '{include_actions}'",
Expand All @@ -85,14 +85,14 @@ def get_include_actions_error_response(include_actions: str) -> dict[str, Any]:
)


def get_category_error_response(category: str) -> dict[str, Any]:
def get_category_error_response(category: str) -> ResponseReturnValue:
diagnostics = f"{category} is not a category that is supported by the API"
return INVALID_CATEGORY_ERROR.log_and_generate_response(
log_message=f"Invalid category query param: '{category}'", diagnostics=diagnostics, location_param="category"
)


def get_condition_error_response(condition: str) -> dict[str, Any]:
def get_condition_error_response(condition: str) -> ResponseReturnValue:
diagnostics = (
f"{condition} should be a single or comma separated list of condition "
f"strings with no other punctuation or special characters"
Expand Down
1 change: 1 addition & 0 deletions src/eligibility_signposting_api/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
AwsAccessKey = NewType("AwsAccessKey", str)
AwsSecretAccessKey = NewType("AwsSecretAccessKey", str)
AwsKinesisFirehoseStreamName = NewType("AwsKinesisFirehoseStreamName", str)
ApiDomainName = NewType("ApiDomainName", str)


@cache
Expand Down
1 change: 1 addition & 0 deletions src/eligibility_signposting_api/config/contants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Literal

URL_PREFIX = "patient-check"
RULE_STOP_DEFAULT = False
NHS_NUMBER_HEADER = "nhs-login-nhs-number"
ALLOWED_CONDITIONS = Literal["COVID", "FLU", "MMR", "RSV"]
Loading