Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e198d26
[NDR-29] Refactoring the service
NogaNHS Apr 24, 2025
4e0442c
[NDR-29] Add new lambda
NogaNHS Apr 28, 2025
d0ea9f9
[NDR-29] Add new lambda to github actions
NogaNHS Apr 28, 2025
33534f9
[NDR-29] refactor name
NogaNHS Apr 28, 2025
864b5b7
Merge branch 'main' into NDR-29
NogaNHS Apr 29, 2025
ee400e0
[NDR-29] return a fhir bundle from search
NogaNHS May 16, 2025
f79fffc
[NDR-29] return url to point to endpoint
NogaNHS May 19, 2025
5e2650c
[NDR-29] change env var name
NogaNHS May 19, 2025
055897b
[NDR-29] add details to doc ref method
NogaNHS May 19, 2025
b22abf6
[NDR-29] add fhir decorators
NogaNHS May 19, 2025
f80ebd3
[NDR-29] change extract_nhs_number_from_event to raise an error when …
NogaNHS May 19, 2025
51197fe
[NDR-29] add querystring
NogaNHS May 20, 2025
eb9d7ac
[NDR-29] add unit tests for build filter expression
NogaNHS May 21, 2025
a6ce877
[NDR-29] fix test for retrieve service
NogaNHS May 21, 2025
475ae84
[NDR-29] fix query filter for uploaded
NogaNHS May 21, 2025
e6ef2b0
[NDR-29] refactor bundle
NogaNHS May 22, 2025
b0fa384
[NDR-29] add access check changes
NogaNHS May 22, 2025
0a1f258
[NDR-29] additional tests
NogaNHS May 23, 2025
f5b09a4
Merge branch 'refs/heads/main' into NDR-29
NogaNHS May 27, 2025
d92a640
[NDR-29] fix import after merge
NogaNHS May 27, 2025
42c91dc
[NDR-29] changes to model after merge
NogaNHS May 27, 2025
b25283f
[NDR-29] add missing param
NogaNHS May 27, 2025
2ab1195
[NDR-29] missing type in url
NogaNHS May 27, 2025
663caad
[NDR-29] fix failing unit tests
NogaNHS May 28, 2025
6097926
[NDR-29] rename create_general_fhir_document_reference_object
NogaNHS May 28, 2025
c24ef3a
[NDR-29] remove _fetch_documents
NogaNHS May 28, 2025
32b6016
[NDR-29] changes lambda error code
NogaNHS Jun 4, 2025
2b56b24
Merge branch 'main' into NDR-29
NogaNHS Jun 4, 2025
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
14 changes: 14 additions & 0 deletions .github/workflows/base-lambdas-reusable-deploy-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -491,3 +491,17 @@ jobs:
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}

deploy_fhir_document_reference_search_lambda:
name: Deploy Search Document References FHIR Lambda
uses: ./.github/workflows/base-lambdas-reusable-deploy.yml
with:
environment: ${{ inputs.environment}}
python_version: ${{ inputs.python_version }}
build_branch: ${{ inputs.build_branch}}
sandbox: ${{ inputs.sandbox }}
lambda_handler_name: fhir_document_reference_search_handler
lambda_aws_name: SearchDocumentReferencesFHIRLambda
lambda_layer_names: 'core_lambda_layer'
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}

5 changes: 5 additions & 0 deletions lambdas/enums/lambda_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ def create_error_body(self, params: Optional[dict] = None) -> str:
DocRefClient = {
"err_code": "DRS_5001",
"message": "An error occurred when searching for available documents",
"fhir_coding": FhirIssueCoding.EXCEPTION,
}

"""
Expand Down Expand Up @@ -537,10 +538,12 @@ def create_error_body(self, params: Optional[dict] = None) -> str:
PatientIdInvalid = {
"err_code": "PN_4001",
"message": "Invalid patient number %(number)s",
"fhir_coding": FhirIssueCoding.INVALID,
}
PatientIdNoKey = {
"err_code": "PN_4002",
"message": "An error occurred due to missing key",
"fhir_coding": FhirIssueCoding.INVALID,
}
PatientIdMismatch = {
"err_code": "PN_4003",
Expand All @@ -554,6 +557,7 @@ def create_error_body(self, params: Optional[dict] = None) -> str:
UploadInProgressError = {
"err_code": "LGL_423",
"message": "Records are in the process of being uploaded",
"fhir_coding": FhirIssueCoding.FORBIDDEN,
}
IncompleteRecordError = {
"err_code": "LGL_400",
Expand Down Expand Up @@ -581,4 +585,5 @@ def create_error_body(self, params: Optional[dict] = None) -> str:
InternalServerError = {
"err_code": "UE_500",
"message": "An internal server error occurred",
"fhir_coding": FhirIssueCoding.EXCEPTION,
}
164 changes: 164 additions & 0 deletions lambdas/handlers/fhir_document_reference_search_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import json
from typing import Any, Dict, Optional, Tuple

from enums.lambda_error import LambdaError
from oauthlib.oauth2 import WebApplicationClient
from services.base.ssm_service import SSMService
from services.document_reference_search_service import DocumentReferenceSearchService
from services.dynamic_configuration_service import DynamicConfigurationService
from services.oidc_service import OidcService
from services.search_patient_details_service import SearchPatientDetailsService
from utils.audit_logging_setup import LoggingService
from utils.decorators.ensure_env_var import ensure_environment_variables
from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions_fhir
from utils.decorators.set_audit_arg import set_request_context_for_logging
from utils.decorators.validate_patient_id import validate_patient_id_fhir
from utils.exceptions import AuthorisationException, OidcApiException
from utils.lambda_exceptions import DocumentRefSearchException, SearchPatientException
from utils.lambda_response import ApiGatewayResponse
from utils.request_context import request_context

logger = LoggingService(__name__)

# Constants
HEADER_AUTHORIZATION = "Authorization"
HEADER_CIS2_USER_ID = "cis2-urid"
PARAM_TYPE_IDENTIFIER = "type:identifier"
PARAM_CUSTODIAN_IDENTIFIER = "custodian:identifier"
PARAM_SUBJECT_IDENTIFIER = "subject:identifier"
PARAM_NEXT_PAGE_TOKEN = "next-page-token"


@ensure_environment_variables(
names=["DYNAMODB_TABLE_LIST", "DOCUMENT_RETRIEVE_ENDPOINT_APIM"]
)
@set_request_context_for_logging
@validate_patient_id_fhir
@handle_lambda_exceptions_fhir
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
"""
Lambda handler for searching document references by NHS number.
Args:
event: API Gateway event containing query parameters with NHS number
context: Lambda context
Returns:
API Gateway response containing FHIR document references or 404 if none is found
"""
logger.info("Received request to search for document references")

bearer_token = extract_bearer_token(event)
selected_role_id = event.get("headers", {}).get(HEADER_CIS2_USER_ID, "")

nhs_number, search_filters = parse_query_parameters(
event.get("queryStringParameters", {})
)
request_context.patient_nhs_no = nhs_number

if selected_role_id:
validate_user_access(bearer_token, selected_role_id, nhs_number)

service = DocumentReferenceSearchService()
document_references = service.get_document_references(
nhs_number=nhs_number,
return_fhir=True,
additional_filters=search_filters,
)

if not document_references:
logger.info(f"No document references found for NHS number: {nhs_number}")
return ApiGatewayResponse(
404,
LambdaError.DocumentReferenceNotFound.create_error_response().create_error_fhir_response(
LambdaError.DocumentReferenceNotFound.value.get("fhir_coding")
),
"GET",
).create_api_gateway_response()
return ApiGatewayResponse(
200, json.dumps(document_references), "GET"
).create_api_gateway_response()


def parse_query_parameters(
query_string: Dict[str, str]
) -> Tuple[Optional[str], Dict[str, str]]:
"""
Parse and extract NHS number and search filters from query parameters.

Args:
query_string: Dictionary of query parameters

Returns:
Tuple of (NHS number, search filters dictionary)
"""
search_filters = {}
nhs_number = None

for key, value in query_string.items():
if key == PARAM_TYPE_IDENTIFIER:
search_filters["file_type"] = value.split("|")[-1]
elif key == PARAM_CUSTODIAN_IDENTIFIER:
search_filters["custodian"] = value.split("|")[-1]
elif key == PARAM_SUBJECT_IDENTIFIER:
nhs_number = value.split("|")[-1]
elif key == PARAM_NEXT_PAGE_TOKEN:
pass # Handled elsewhere
else:
logger.warning(f"Unknown query parameter: {key}")

return nhs_number, search_filters


def validate_user_access(
bearer_token: str, selected_role_id: str, nhs_number: str
) -> None:
"""
Validate that the user has permission to access the requested patient data.

Args:
bearer_token: Authentication token
selected_role_id: CIS2 user role ID
nhs_number: NHS number to validate access for

Raises:
DocumentRefSearchException: If the user doesn't have permission
"""
logger.info("Detected a cis2 user access request, checking for access permission")

# Initialize services
configuration_service = DynamicConfigurationService()
configuration_service.set_auth_ssm_prefix()

try:
# Authenticate user and get role information
oidc_service = OidcService()
oidc_service.set_up_oidc_parameters(SSMService, WebApplicationClient)
userinfo = oidc_service.fetch_userinfo(bearer_token)
org_ods_code = oidc_service.fetch_user_org_code(userinfo, selected_role_id)
smartcard_role_code, _ = oidc_service.fetch_user_role_code(
userinfo, selected_role_id, "R"
)
except (OidcApiException, AuthorisationException) as e:
logger.error(f"Authorization failed: {e}")
raise DocumentRefSearchException(403, LambdaError.DocumentReferenceUnauthorised)

try:
# Validate patient access
search_patient_service = SearchPatientDetailsService(
smartcard_role_code, org_ods_code
)
search_patient_service.handle_search_patient_request(nhs_number, False)
except SearchPatientException as e:
raise DocumentRefSearchException(e.status_code, e.error)


def extract_bearer_token(event):
"""Extract and validate bearer token from event"""
headers = event.get("headers", {})
if not headers:
logger.warning("No headers found in request")
raise DocumentRefSearchException(401, LambdaError.DocumentReferenceUnauthorised)
bearer_token = headers.get("Authorization", None)
if not bearer_token or not bearer_token.startswith("Bearer "):
logger.warning("No bearer token found in request")
raise DocumentRefSearchException(401, LambdaError.DocumentReferenceUnauthorised)
return bearer_token
Loading