Skip to content

Commit 4db993d

Browse files
[PRMP-813] Enable pilot practices to search patients outside of their ods (#902)
Co-authored-by: adamwhitingnhs <[email protected]>
1 parent 74fd7e3 commit 4db993d

12 files changed

+183
-71
lines changed

lambdas/handlers/search_patient_details_handler.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
from enums.feature_flags import FeatureFlags
12
from enums.lambda_error import LambdaError
23
from enums.logging_app_interaction import LoggingAppInteraction
4+
from services.feature_flags_service import FeatureFlagService
35
from services.search_patient_details_service import SearchPatientDetailsService
46
from utils.audit_logging_setup import LoggingService
57
from utils.decorators.ensure_env_var import ensure_environment_variables
@@ -38,13 +40,27 @@ def lambda_handler(event, context):
3840
)
3941
raise SearchPatientException(400, LambdaError.SearchPatientMissing)
4042

43+
feature_flag_service = FeatureFlagService()
44+
feature_flag = feature_flag_service.get_feature_flags_by_flag(
45+
FeatureFlags.UPLOAD_DOCUMENT_ITERATION_3_ENABLED
46+
)
47+
document_upload_iteration3_enabled = feature_flag[
48+
FeatureFlags.UPLOAD_DOCUMENT_ITERATION_3_ENABLED
49+
]
50+
can_access_not_my_record = (
51+
feature_flag_service.check_if_ods_code_is_in_pilot()
52+
and document_upload_iteration3_enabled
53+
)
54+
4155
search_service = SearchPatientDetailsService(
4256
user_role=user_role, user_ods_code=user_ods_code
4357
)
4458

4559
# Get patient details from service
4660
patient_details = search_service.handle_search_patient_request(
47-
nhs_number,
61+
nhs_number=nhs_number,
62+
update_session=True,
63+
can_access_not_my_record=can_access_not_my_record,
4864
)
4965
formatted_response = patient_details.model_dump_json(
5066
by_alias=True,

lambdas/models/pds_models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class PatientDetails(BaseModel):
8686
active: Optional[bool] = None
8787
deceased: bool = False
8888
death_notification_status: Optional[DeathNotificationStatus] = None
89+
can_manage_record: Optional[bool] = None
8990

9091

9192
class Patient(BaseModel):

lambdas/scripts/dynamodb_migration_prmp_198.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import os
2-
import boto3
32
from typing import Callable, Iterable
4-
from boto3.dynamodb.conditions import Key, Attr
53

64
from scripts.MigrationBase import MigrationBase
75
from services.base.dynamo_service import DynamoDBService
86
from utils.audit_logging_setup import LoggingService
9-
from utils.exceptions import MigrationUnrecoverableException, MigrationRetryableException
7+
from utils.exceptions import (
8+
MigrationRetryableException,
9+
MigrationUnrecoverableException,
10+
)
1011

1112

1213
class AuthorMigration(MigrationBase):
@@ -77,7 +78,8 @@ def get_update_author_items(self, entry: dict) -> dict | None:
7778
f"No completed bulk upload found for NHS number: {nhs_number}"
7879
)
7980
raise MigrationUnrecoverableException(
80-
message=f"No completed bulk upload found for NHS number: {nhs_number}", item_id=entry.get("ID")
81+
message=f"No completed bulk upload found for NHS number: {nhs_number}",
82+
item_id=entry.get("ID"),
8183
)
8284

8385
new_author = bulk_upload_row.get("UploaderOdsCode")
@@ -86,7 +88,8 @@ def get_update_author_items(self, entry: dict) -> dict | None:
8688
f"No uploader ODS code found for NHS number: {nhs_number}"
8789
)
8890
raise MigrationUnrecoverableException(
89-
message=f"No uploader ODS code found for NHS number: {nhs_number}", item_id=entry.get("ID")
91+
message=f"No uploader ODS code found for NHS number: {nhs_number}",
92+
item_id=entry.get("ID"),
9093
)
9194

9295
return {"Author": new_author}
@@ -105,7 +108,7 @@ def build_bulk_upload_lookup(self, entries: Iterable[dict]) -> dict[str, dict]:
105108
nhs = entry.get("NhsNumber")
106109
if nhs:
107110
unique_nhs_numbers.add(nhs)
108-
111+
109112
self.logger.info(f"Found {len(unique_nhs_numbers)} unique NHS numbers to query")
110113

111114
if not unique_nhs_numbers:
@@ -121,9 +124,9 @@ def build_bulk_upload_lookup(self, entries: Iterable[dict]) -> dict[str, dict]:
121124
table_name=self.bulk_upload_report_table,
122125
search_key="NhsNumber",
123126
search_condition=nhs_number,
124-
index_name="NhsNumberIndex",
127+
index_name="NhsNumberIndex",
125128
)
126-
129+
127130
# Find the most recent completed upload for this NHS number
128131
for row in items:
129132
status = row.get("UploadStatus")

lambdas/services/authoriser_service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def deny_access_policy(self, path, user_role, nhs_number: str = None):
7272
patient_access_is_allowed = (
7373
nhs_number in self.allowed_nhs_numbers if nhs_number else False
7474
)
75+
7576
access_to_deceased_patient = (
7677
nhs_number in self.deceased_nhs_numbers if nhs_number else False
7778
)

lambdas/services/document_reference_search_service.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from enums.metadata_field_names import DocumentReferenceMetadataFields
1010
from enums.mtls import MtlsCommonNames
1111
from enums.snomed_codes import SnomedCodes
12-
from enums.supported_document_types import SupportedDocumentTypes
1312
from models.document_reference import DocumentReference
1413
from models.fhir.R4.bundle import Bundle, BundleEntry
1514
from models.fhir.R4.fhir_document_reference import Attachment, DocumentReferenceInfo

lambdas/services/search_patient_details_service.py

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
from enums.feature_flags import FeatureFlags
21
from enums.lambda_error import LambdaError
32
from enums.repository_role import RepositoryRole
43
from pydantic import ValidationError
54
from pydantic_core import PydanticSerializationError
6-
from services.feature_flags_service import FeatureFlagService
75
from services.manage_user_session_access import ManageUserSessionAccess
86
from utils.audit_logging_setup import LoggingService
97
from utils.exceptions import (
@@ -24,9 +22,13 @@ def __init__(self, user_role, user_ods_code):
2422
self.user_role = user_role
2523
self.user_ods_code = user_ods_code
2624
self.manage_user_session_service = ManageUserSessionAccess()
27-
self.feature_flag_service = FeatureFlagService()
2825

29-
def handle_search_patient_request(self, nhs_number, update_session=True):
26+
def handle_search_patient_request(
27+
self,
28+
nhs_number,
29+
update_session=True,
30+
can_access_not_my_record=False,
31+
):
3032
"""
3133
Handle search patient request and return patient details if authorised.
3234
@@ -43,15 +45,20 @@ def handle_search_patient_request(self, nhs_number, update_session=True):
4345
try:
4446
patient_details = self._fetch_patient_details(nhs_number)
4547

48+
can_manage_record = patient_details.deceased
49+
4650
if not patient_details.deceased:
47-
self._check_authorization(patient_details.general_practice_ods)
51+
can_manage_record = self._check_authorization(
52+
patient_details.general_practice_ods, can_access_not_my_record
53+
)
4854

4955
logger.info("Searched for patient details", {"Result": "Patient found"})
5056

51-
if update_session:
57+
if update_session and can_manage_record:
5258
self._update_session(nhs_number, patient_details.deceased)
5359

54-
# Return the patient details object directly
60+
patient_details.can_manage_record = can_manage_record
61+
5562
return patient_details
5663

5764
except PatientNotFoundException as e:
@@ -86,7 +93,9 @@ def _fetch_patient_details(self, nhs_number):
8693
pds_service = get_pds_service()
8794
return pds_service.fetch_patient_details(nhs_number)
8895

89-
def _check_authorization(self, gp_ods_for_patient):
96+
def _check_authorization(
97+
self, gp_ods_for_patient, can_access_not_my_record
98+
) -> bool:
9099
"""
91100
Check if the current user is authorised to view the patient details.
92101
@@ -97,33 +106,18 @@ def _check_authorization(self, gp_ods_for_patient):
97106
UserNotAuthorisedException: If the user is not authorised
98107
"""
99108
patient_is_active = is_ods_code_active(gp_ods_for_patient)
100-
is_arf_journey_on = self._is_arf_upload_enabled()
109+
user_is_data_controller = gp_ods_for_patient == self.user_ods_code
101110

102111
match self.user_role:
103-
case RepositoryRole.GP_ADMIN.value:
104-
if patient_is_active and gp_ods_for_patient != self.user_ods_code:
105-
raise UserNotAuthorisedException
106-
elif not patient_is_active and not is_arf_journey_on:
107-
raise UserNotAuthorisedException
108-
109-
case RepositoryRole.GP_CLINICAL.value:
110-
if not patient_is_active or gp_ods_for_patient != self.user_ods_code:
111-
raise UserNotAuthorisedException
112+
case RepositoryRole.GP_ADMIN.value | RepositoryRole.GP_CLINICAL.value:
113+
if user_is_data_controller or can_access_not_my_record:
114+
return user_is_data_controller
112115

113116
case RepositoryRole.PCSE.value:
114-
if patient_is_active:
115-
raise UserNotAuthorisedException
117+
if not patient_is_active:
118+
return True
116119

117-
case _:
118-
raise UserNotAuthorisedException
119-
120-
def _is_arf_upload_enabled(self):
121-
"""Check if ARF upload workflow is enabled via feature flags"""
122-
upload_flag_name = FeatureFlags.UPLOAD_ARF_WORKFLOW_ENABLED.value
123-
upload_lambda_enabled_flag_object = (
124-
self.feature_flag_service.get_feature_flags_by_flag(upload_flag_name)
125-
)
126-
return upload_lambda_enabled_flag_object[upload_flag_name]
120+
raise UserNotAuthorisedException
127121

128122
def _update_session(self, nhs_number, is_deceased):
129123
"""Update the user session with permitted search information"""

lambdas/tests/e2e/api/fhir/test_upload_document_fhir_api_failure.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
create_mtls_session,
1212
fetch_with_retry_mtls,
1313
)
14-
from lambdas.tests.e2e.conftest import APIM_ENDPOINT, PDM_SNOMED
1514
from lambdas.tests.e2e.helpers.data_helper import PdmDataHelper
1615

1716
pdm_data_helper = PdmDataHelper()
@@ -60,7 +59,9 @@ def test_create_document_virus(test_data):
6059
}
6160

6261
# Attach EICAR data
63-
eicar_string = r"X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
62+
eicar_string = (
63+
r"X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
64+
)
6465
record["data"] = base64.b64encode(eicar_string.encode()).decode()
6566
payload = pdm_data_helper.create_upload_payload(record)
6667

@@ -74,8 +75,8 @@ def test_create_document_virus(test_data):
7475
def condition(response_json):
7576
logging.info(response_json)
7677
return response_json.get("docStatus") in (
77-
"cancelled",
78-
"final",
78+
"cancelled",
79+
"final",
7980
)
8081

8182
raw_retrieve_response = retrieve_document_with_retry(

lambdas/tests/e2e/api/fhir/test_upload_document_fhir_api_success.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
import logging
33
import os
44

5-
import pytest
6-
import requests
7-
85
from lambdas.tests.e2e.api.fhir.conftest import (
96
MTLS_ENDPOINT,
107
create_mtls_session,

lambdas/tests/unit/handlers/test_get_document_reference_handler.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ def mock_interaction_id():
4343
@pytest.fixture
4444
def mocked_bad_env_vars():
4545
env_vars = {
46-
# "LLOYD_GEORGE_DYNAMODB_NAME": "mock_dynamodb_name",
4746
"PRESIGNED_ASSUME_ROLE": "mock_presigned_role",
4847
"APPCONFIG_APPLICATION": "mock_value",
4948
"APPCONFIG_ENVIRONMENT": "mock_value",

lambdas/tests/unit/handlers/test_search_patient_details_handler.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77
from handlers.search_patient_details_handler import lambda_handler
88
from models.pds_models import PatientDetails
9+
from services.feature_flags_service import FeatureFlagService
910
from utils.lambda_exceptions import SearchPatientException
1011
from utils.lambda_response import ApiGatewayResponse
1112

@@ -40,12 +41,23 @@ def mocked_context(mocker):
4041
)
4142

4243

44+
@pytest.fixture
45+
def mock_check_if_ods_code_is_in_pilot(mocker):
46+
mock_function = mocker.patch.object(
47+
FeatureFlagService, "check_if_ods_code_is_in_pilot"
48+
)
49+
ods_in_pilot = mock_function.return_value = True
50+
yield ods_in_pilot
51+
52+
4353
def test_lambda_handler_valid_id_returns_200(
4454
set_env,
4555
valid_id_event_with_auth_header,
4656
context,
4757
mocker,
4858
mocked_context,
59+
mock_upload_document_iteration_3_enabled,
60+
mock_check_if_ods_code_is_in_pilot,
4961
):
5062
patient_details_object = PatientDetails(
5163
givenName=["Jane"],
@@ -100,7 +112,13 @@ def test_lambda_handler_invalid_id_returns_400(invalid_id_event, context):
100112

101113

102114
def test_lambda_handler_valid_id_not_in_pds_returns_404(
103-
set_env, valid_id_event_with_auth_header, context, mocker, mocked_context
115+
set_env,
116+
valid_id_event_with_auth_header,
117+
context,
118+
mocker,
119+
mocked_context,
120+
mock_upload_document_iteration_3_enabled,
121+
mock_check_if_ods_code_is_in_pilot,
104122
):
105123
mocker.patch(
106124
"handlers.search_patient_details_handler.SearchPatientDetailsService.handle_search_patient_request",

0 commit comments

Comments
 (0)