diff --git a/Makefile b/Makefile index 8b44ad164..a2891afc5 100644 --- a/Makefile +++ b/Makefile @@ -105,7 +105,7 @@ publish-ci-image: ## Publish the CI image test: check-warn ## Run the unit tests @echo "Running unit tests" - pytest --ignore=tests/smoke $(TEST_ARGS) + PYTHONPATH=. poetry run pytest --ignore tests/smoke $(TEST_ARGS) test-features-integration: check-warn ## Run the BDD feature tests in the integration environment @echo "Running feature tests in the integration environment ${TF_WORKSPACE_NAME}" diff --git a/api/consumer/readDocumentReference/tests/test_read_document_reference_consumer.py b/api/consumer/readDocumentReference/tests/test_read_document_reference_consumer.py index b393d8c59..27a4b1c62 100644 --- a/api/consumer/readDocumentReference/tests/test_read_document_reference_consumer.py +++ b/api/consumer/readDocumentReference/tests/test_read_document_reference_consumer.py @@ -16,7 +16,9 @@ @mock_aws @mock_repository -def test_read_document_reference_happy_path(repository: DocumentPointerRepository): +def test_read_document_reference_happy_path( + repository: DocumentPointerRepository, +): # Create the document pointer doc_ref = load_document_reference("Y05868-736253002-Valid") doc_pointer = DocumentPointer.from_document_reference(doc_ref) diff --git a/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py b/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py index ec1c9fd67..d65214402 100644 --- a/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py +++ b/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py @@ -24,7 +24,9 @@ @mock_aws @mock_repository -def test_search_document_reference_happy_path(repository: DocumentPointerRepository): +def test_search_document_reference_happy_path( + repository: DocumentPointerRepository, +): doc_ref = load_document_reference("Y05868-736253002-Valid") doc_pointer = DocumentPointer.from_document_reference(doc_ref) repository.create(doc_pointer) @@ -516,7 +518,9 @@ def test_search_document_reference_happy_path_with_nicip_type( @mock_aws @mock_repository -def test_search_document_reference_no_results(repository: DocumentPointerRepository): +def test_search_document_reference_no_results( + repository: DocumentPointerRepository, +): event = create_test_api_gateway_event( headers=create_headers(), query_string_parameters={ @@ -633,7 +637,9 @@ def test_search_document_reference_invalid_nhs_number( @mock_aws @mock_repository -def test_search_document_reference_invalid_type(repository: DocumentPointerRepository): +def test_search_document_reference_invalid_type( + repository: DocumentPointerRepository, +): event = create_test_api_gateway_event( headers=create_headers(), query_string_parameters={ diff --git a/api/consumer/searchPostDocumentReference/tests/test_search_post_document_reference_consumer.py b/api/consumer/searchPostDocumentReference/tests/test_search_post_document_reference_consumer.py index 0414c531f..359b75c7f 100644 --- a/api/consumer/searchPostDocumentReference/tests/test_search_post_document_reference_consumer.py +++ b/api/consumer/searchPostDocumentReference/tests/test_search_post_document_reference_consumer.py @@ -268,7 +268,9 @@ def test_search_post_document_reference_happy_path_with_multiple_categories( @mock_aws @mock_repository -def test_search_document_reference_no_results(repository: DocumentPointerRepository): +def test_search_document_reference_no_results( + repository: DocumentPointerRepository, +): event = create_test_api_gateway_event( headers=create_headers(), body=json.dumps( diff --git a/api/producer/createDocumentReference/tests/test_create_document_reference.py b/api/producer/createDocumentReference/tests/test_create_document_reference.py index a737da62d..00871bd7c 100644 --- a/api/producer/createDocumentReference/tests/test_create_document_reference.py +++ b/api/producer/createDocumentReference/tests/test_create_document_reference.py @@ -31,7 +31,9 @@ @mock_repository @freeze_time("2024-03-21T12:34:56.789") @freeze_uuid("00000000-0000-0000-0000-000000000001") -def test_create_document_reference_happy_path(repository: DocumentPointerRepository): +def test_create_document_reference_happy_path( + repository: DocumentPointerRepository, +): doc_ref_data = load_document_reference_data("Y05868-736253002-Valid") event = create_test_api_gateway_event( @@ -1610,7 +1612,7 @@ def test_create_document_reference_with_date_and_meta_lastupdated_ignored( @mock_repository @freeze_time("2024-03-21T12:34:56.789") @freeze_uuid("00000000-0000-0000-0000-000000000001") -def test_create_document_reference_with_date_overidden( +def test_create_document_reference_with_date_overridden( repository: DocumentPointerRepository, ): doc_ref_data = load_document_reference_data("Y05868-736253002-Valid-with-date") diff --git a/api/producer/deleteDocumentReference/tests/test_delete_document_reference.py b/api/producer/deleteDocumentReference/tests/test_delete_document_reference.py index 41a28e9d6..f7061b69c 100644 --- a/api/producer/deleteDocumentReference/tests/test_delete_document_reference.py +++ b/api/producer/deleteDocumentReference/tests/test_delete_document_reference.py @@ -16,7 +16,9 @@ @mock_aws @mock_repository -def test_delete_document_reference_happy_path(repository: DocumentPointerRepository): +def test_delete_document_reference_happy_path( + repository: DocumentPointerRepository, +): doc_ref = load_document_reference("Y05868-736253002-Valid") doc_pointer = DocumentPointer.from_document_reference(doc_ref) repository.create(doc_pointer) @@ -131,7 +133,9 @@ def test_delete_document_reference_invalid_producer_id(): @mock_aws @mock_repository -def test_delete_document_reference_not_exists(repository: DocumentPointerRepository): +def test_delete_document_reference_not_exists( + repository: DocumentPointerRepository, +): event = create_test_api_gateway_event( headers=create_headers(), path_parameters={"id": "Y05868-99999-99999-999999"} ) diff --git a/api/producer/readDocumentReference/tests/test_read_document_reference_producer.py b/api/producer/readDocumentReference/tests/test_read_document_reference_producer.py index 8d788dc5f..1c96fac65 100644 --- a/api/producer/readDocumentReference/tests/test_read_document_reference_producer.py +++ b/api/producer/readDocumentReference/tests/test_read_document_reference_producer.py @@ -16,7 +16,9 @@ @mock_aws @mock_repository -def test_read_document_reference_happy_path(repository: DocumentPointerRepository): +def test_read_document_reference_happy_path( + repository: DocumentPointerRepository, +): # Create the document pointer doc_ref = load_document_reference("Y05868-736253002-Valid") doc_pointer = DocumentPointer.from_document_reference(doc_ref) @@ -151,7 +153,9 @@ def test_read_document_reference_incorrect_ods_code(): @mock_aws @mock_repository -def test_read_document_reference_invalid_json(repository: DocumentPointerRepository): +def test_read_document_reference_invalid_json( + repository: DocumentPointerRepository, +): doc_ref = load_document_reference("Y05868-736253002-Valid") doc_pointer = DocumentPointer.from_document_reference(doc_ref) doc_pointer.document = "invalid json" diff --git a/api/producer/searchDocumentReference/tests/test_search_document_reference_producer.py b/api/producer/searchDocumentReference/tests/test_search_document_reference_producer.py index b7683d1d0..5f80a8d2e 100644 --- a/api/producer/searchDocumentReference/tests/test_search_document_reference_producer.py +++ b/api/producer/searchDocumentReference/tests/test_search_document_reference_producer.py @@ -23,7 +23,9 @@ @mock_aws @mock_repository -def test_search_document_reference_happy_path(repository: DocumentPointerRepository): +def test_search_document_reference_happy_path( + repository: DocumentPointerRepository, +): doc_ref = load_document_reference("Y05868-736253002-Valid") doc_pointer = DocumentPointer.from_document_reference(doc_ref) repository.create(doc_pointer) @@ -55,7 +57,9 @@ def test_search_document_reference_happy_path(repository: DocumentPointerReposit @mock_aws @mock_repository -def test_search_document_reference_no_results(repository: DocumentPointerRepository): +def test_search_document_reference_no_results( + repository: DocumentPointerRepository, +): event = create_test_api_gateway_event( headers=create_headers(), query_string_parameters={ @@ -168,7 +172,9 @@ def test_search_document_reference_invalid_nhs_number( @mock_aws @mock_repository -def test_search_document_reference_invalid_type(repository: DocumentPointerRepository): +def test_search_document_reference_invalid_type( + repository: DocumentPointerRepository, +): event = create_test_api_gateway_event( headers=create_headers(), query_string_parameters={ diff --git a/api/producer/searchPostDocumentReference/tests/test_search_post_document_reference_producer.py b/api/producer/searchPostDocumentReference/tests/test_search_post_document_reference_producer.py index 8948a9cee..ed8df5915 100644 --- a/api/producer/searchPostDocumentReference/tests/test_search_post_document_reference_producer.py +++ b/api/producer/searchPostDocumentReference/tests/test_search_post_document_reference_producer.py @@ -25,7 +25,9 @@ @mock_aws @mock_repository -def test_search_document_reference_happy_path(repository: DocumentPointerRepository): +def test_search_document_reference_happy_path( + repository: DocumentPointerRepository, +): doc_ref = load_document_reference("Y05868-736253002-Valid") doc_pointer = DocumentPointer.from_document_reference(doc_ref) repository.create(doc_pointer) @@ -59,7 +61,9 @@ def test_search_document_reference_happy_path(repository: DocumentPointerReposit @mock_aws @mock_repository -def test_search_document_reference_no_results(repository: DocumentPointerRepository): +def test_search_document_reference_no_results( + repository: DocumentPointerRepository, +): event = create_test_api_gateway_event( headers=create_headers(), body=json.dumps( @@ -171,7 +175,9 @@ def test_search_document_reference_invalid_nhs_number( @mock_aws @mock_repository -def test_search_document_reference_invalid_type(repository: DocumentPointerRepository): +def test_search_document_reference_invalid_type( + repository: DocumentPointerRepository, +): event = create_test_api_gateway_event( headers=create_headers(), body=json.dumps( diff --git a/api/producer/updateDocumentReference/tests/test_update_document_reference.py b/api/producer/updateDocumentReference/tests/test_update_document_reference.py index 91fc8a3f6..8d50e1ea4 100644 --- a/api/producer/updateDocumentReference/tests/test_update_document_reference.py +++ b/api/producer/updateDocumentReference/tests/test_update_document_reference.py @@ -23,7 +23,9 @@ @mock_aws @mock_repository @freeze_time("2024-03-21T12:34:56.789") -def test_update_document_reference_happy_path(repository: DocumentPointerRepository): +def test_update_document_reference_happy_path( + repository: DocumentPointerRepository, +): doc_ref = load_document_reference("Y05868-736253002-Valid") doc_pointer = DocumentPointer.from_document_reference(doc_ref) repository.create(doc_pointer) @@ -629,7 +631,9 @@ def test_update_document_reference_immutable_fields(repository): @mock_aws @mock_repository -def test_update_document_reference_cannot_change_status_to_not_current(repository): +def test_update_document_reference_cannot_change_status_to_not_current( + repository, +): doc_ref = load_document_reference("Y05868-736253002-Valid") doc_pointer = DocumentPointer.from_document_reference(doc_ref) repository.create(doc_pointer) @@ -677,7 +681,9 @@ def test_update_document_reference_cannot_change_status_to_not_current(repositor @mock_aws @mock_repository -def test_update_document_reference_with_no_context_related_for_ssp_url(repository): +def test_update_document_reference_with_no_context_related_for_ssp_url( + repository, +): doc_ref = load_document_reference("Y05868-736253002-Valid-with-ssp-content") doc_pointer = DocumentPointer.from_document_reference(doc_ref) repository.create(doc_pointer) diff --git a/api/producer/upsertDocumentReference/tests/test_upsert_document_reference.py b/api/producer/upsertDocumentReference/tests/test_upsert_document_reference.py index 5b464c79a..082257f52 100644 --- a/api/producer/upsertDocumentReference/tests/test_upsert_document_reference.py +++ b/api/producer/upsertDocumentReference/tests/test_upsert_document_reference.py @@ -29,7 +29,9 @@ @mock_aws @mock_repository @freeze_time("2024-03-21T12:34:56.789") -def test_upsert_document_reference_happy_path(repository: DocumentPointerRepository): +def test_upsert_document_reference_happy_path( + repository: DocumentPointerRepository, +): doc_ref_data = load_document_reference_data("Y05868-736253002-Valid") event = create_test_api_gateway_event( @@ -1578,7 +1580,7 @@ def test_upsert_document_reference_with_date_and_meta_lastupdated_ignored( @mock_aws @mock_repository @freeze_time("2024-03-21T12:34:56.789") -def test_upsert_document_reference_with_date_overidden( +def test_upsert_document_reference_with_date_overridden( repository: DocumentPointerRepository, ): doc_ref_data = load_document_reference_data("Y05868-736253002-Valid-with-date") diff --git a/layer/nrlf/core/authoriser.py b/layer/nrlf/core/authoriser.py index 227179dd7..a87a6a07e 100644 --- a/layer/nrlf/core/authoriser.py +++ b/layer/nrlf/core/authoriser.py @@ -1,4 +1,5 @@ import json +import re import sys from os import path @@ -9,6 +10,39 @@ from nrlf.core.logger import LogReference, logger from nrlf.core.model import ConnectionMetadata +default_lookup_path = "/opt/python/nrlf_permissions" + + +def get_pointer_permissions_v2( + connection_metadata: ConnectionMetadata, + request_path: str, + lookup_path=default_lookup_path, +): + producer_or_consumer = ( + re.search("^/(producer|consumer)/", request_path).group().strip("/") + ) + + ods_code = connection_metadata.ods_code + app_id = connection_metadata.nrl_app_id + + key = f"{producer_or_consumer}/{app_id}/{ods_code}.json" + logger.log(LogReference.V2PERMISSIONS011, key=key) + + file_path = f"{lookup_path}/{key}" + + pointer_permissions = {} + try: + with open(file_path) as file: + pointer_permissions = json.load(file) + except Exception as exc: + logger.log( + LogReference.V2PERMISSIONS014, + exc_info=sys.exc_info(), + stacklevel=5, + error=str(exc), + ) + return pointer_permissions + def get_pointer_types( connection_metadata: ConnectionMetadata, config: Config diff --git a/layer/nrlf/core/decorators.py b/layer/nrlf/core/decorators.py index f2dc724db..72bfba3fe 100644 --- a/layer/nrlf/core/decorators.py +++ b/layer/nrlf/core/decorators.py @@ -11,10 +11,16 @@ from aws_lambda_powertools.utilities.typing import LambdaContext from pydantic import BaseModel -from nrlf.core.authoriser import get_pointer_types, parse_permissions_file +from nrlf.core.authoriser import ( + get_pointer_permissions_v2, + get_pointer_types, + parse_permissions_file, +) from nrlf.core.codes import SpineErrorConcept from nrlf.core.config import Config from nrlf.core.constants import ( + CLIENT_RP_DETAILS, + CONNECTION_METADATA, NHSD_CORRELATION_ID_HEADER, PERMISSION_ALLOW_ALL_POINTER_TYPES, X_CORRELATION_ID_HEADER, @@ -137,12 +143,39 @@ def wrapper(*args, **kwargs) -> Dict[str, Any]: RepositoryType = Union[Type[DocumentPointerRepository], None] -def load_connection_metadata(headers: Dict[str, str], config: Config): - logger.log(LogReference.HANDLER002, headers=headers) - metadata = parse_headers(headers) - logger.log(LogReference.HANDLER003, metadata=metadata.model_dump()) +def _use_v2_permissions_model(headers: Dict[str, str]) -> bool: + case_insensitive_headers = {key.lower(): value for key, value in headers.items()} + # if either or both headers are missing + return ( + CLIENT_RP_DETAILS not in case_insensitive_headers.keys() + or CONNECTION_METADATA not in case_insensitive_headers.keys() + ) + + +def _load_v2_connection_metadata(headers: Dict[str, str], path: str): + logger.log(LogReference.HANDLER004d) + metadata = parse_headers(headers, use_v2_permissions=True) + + logger.log(LogReference.HANDLER004e) + pointer_permissions = get_pointer_permissions_v2(metadata, path) + + metadata.pointer_types = pointer_permissions.get("types", []) + + logger.log( + LogReference.HANDLER004f, pointer_types=metadata.pointer_types + ) # TODO: log other permissions as they're added + + return metadata + + +def load_connection_metadata(headers: Dict[str, str], config: Config, path=""): + + if _use_v2_permissions_model(headers): + return _load_v2_connection_metadata(headers, path) + + metadata = parse_headers(headers, use_v2_permissions=False) if PERMISSION_ALLOW_ALL_POINTER_TYPES in metadata.nrl_permissions: - logger.log(LogReference.HANDLER004a) + logger.log(LogReference.HANDLER004b) metadata.pointer_types = PointerTypes.list() return metadata @@ -262,7 +295,7 @@ def wrapper(event: APIGatewayProxyEvent, context: LambdaContext, **kwargs): config = Config() logger.log(LogReference.HANDLER001, config=config.model_dump()) - metadata = load_connection_metadata(event.headers, config) + metadata = load_connection_metadata(event.headers, config, event.path) if metadata.pointer_types == []: logger.log( diff --git a/layer/nrlf/core/log_references.py b/layer/nrlf/core/log_references.py index eb4fbd33e..d6dcf14b9 100644 --- a/layer/nrlf/core/log_references.py +++ b/layer/nrlf/core/log_references.py @@ -22,10 +22,19 @@ class LogReference(Enum): HANDLER001 = _Reference("DEBUG", "Loaded config from environment variables") HANDLER002 = _Reference("DEBUG", "Attempting to parse request headers") HANDLER003 = _Reference("INFO", "Parsed metadata from request headers") + HANDLER003a = _Reference( + "WARN", "Missing nhsd-end-user-organisation-ods header for v2 permissions" + ) + HANDLER003b = _Reference( + "WARN", "Missing nhsd-nrl-app-id header for v2 permissions" + ) HANDLER004 = _Reference("INFO", "Authorisation lookup enabled") HANDLER004a = _Reference("INFO", "Authorisation lookup skipped for sync request") HANDLER004b = _Reference("INFO", "Parsing embedded permissions file from S3") HANDLER004c = _Reference("INFO", "Parsed embedded permissions file from S3") + HANDLER004d = _Reference("INFO", "Using v2 permissions model") + HANDLER004e = _Reference("INFO", "Parsing v2 permissions file from lambda layer") + HANDLER004f = _Reference("INFO", "Parsed v2 permissions file from lambda layer") HANDLER005 = _Reference("WARN", "Rejecting request due to missing pointer types") HANDLER006 = _Reference("DEBUG", "Attempting to parse request parameters") HANDLER007 = _Reference("INFO", "Parsed request parameters") @@ -58,10 +67,10 @@ class LogReference(Enum): "WARN", "An ParseError occurred whilst processing the request" ) ERROR003 = _Reference( - "WARN", "An unhandler exception occurred whilst handling response headers" + "WARN", "An unhandled exception occurred whilst handling response headers" ) - # S3 Permissions Lookup Logs + # S3 / Embedded Permissions Lookup Logs S3PERMISSIONS001 = _Reference("INFO", "Retrieving pointer types from S3 bucket") S3PERMISSIONS002 = _Reference("INFO", "Retrieved list of pointer types from S3") S3PERMISSIONS003 = _Reference("WARN", "No permissions file found in S3") @@ -70,7 +79,22 @@ class LogReference(Enum): ) S3PERMISSIONS005 = _Reference( "EXCEPTION", - "An error occurred whilst pasrsing embedded permissions files from S3", + "An error occurred whilst parsing embedded permissions files", + ) + + # V2 Embedded Permissions Lookup Logs + V2PERMISSIONS011 = _Reference( + "INFO", "Retrieving v2 pointer permissions from lambda layer" + ) + V2PERMISSIONS012 = _Reference( + "INFO", "Retrieved v2 pointer permissions from lambda layer" + ) + V2PERMISSIONS013 = _Reference( + "WARN", "No v2 permissions file found in lambda layer" + ) + V2PERMISSIONS014 = _Reference( + "EXCEPTION", + "An error occurred whilst retrieving v2 pointer permissions", ) # Parse Logs diff --git a/layer/nrlf/core/model.py b/layer/nrlf/core/model.py index 7163d339f..e1bde2603 100644 --- a/layer/nrlf/core/model.py +++ b/layer/nrlf/core/model.py @@ -50,6 +50,7 @@ class ClientRpDetails(BaseModel): developer_app_id: StrictStr = Field(alias="developer.app.id") +# expand with other permissions types: pointer_types, etc class ConnectionMetadata(BaseModel): pointer_types: list[str] = Field(alias="nrl.pointer-types", default_factory=list) ods_code: str = Field(alias="nrl.ods-code") diff --git a/layer/nrlf/core/request.py b/layer/nrlf/core/request.py index 499e40889..c3aa12318 100644 --- a/layer/nrlf/core/request.py +++ b/layer/nrlf/core/request.py @@ -11,19 +11,50 @@ from nrlf.core.model import ClientRpDetails, ConnectionMetadata -def parse_headers(headers: Dict[str, str]) -> ConnectionMetadata: +def _fetch_ods_app_id_headers(headers: dict[str, str]): + + case_insensitive_headers = {key.lower(): value for key, value in headers.items()} + + ods_code = case_insensitive_headers.get("nhsd-end-user-organisation-ods") + + if not ods_code or len(ods_code.strip()) == 0: + logger.log( + code=LogReference.HANDLER003a, + headers_names=list(case_insensitive_headers.keys()), + ) + + nrl_app_id = case_insensitive_headers.get("nhsd-nrl-app-id") + if not nrl_app_id or len(nrl_app_id.strip()) == 0: + logger.log( + code=LogReference.HANDLER003b, + headers_names=list(case_insensitive_headers.keys()), + ) + + return ods_code, nrl_app_id + + +def parse_headers( + headers: Dict[str, str], use_v2_permissions=False +) -> ConnectionMetadata: """ Parses the connection metadata and client rp details from the headers passed from Apigee """ case_insensitive_headers = {key.lower(): value for key, value in headers.items()} try: - raw_connection_metadata = json.loads( - case_insensitive_headers.get(CONNECTION_METADATA, "{}") - ) raw_client_rp_details = json.loads( case_insensitive_headers.get(CLIENT_RP_DETAILS, "{}") ) + raw_connection_metadata = json.loads( + case_insensitive_headers.get(CONNECTION_METADATA, "{}") + ) + + if use_v2_permissions: + ods_code, nrl_app_id = _fetch_ods_app_id_headers(case_insensitive_headers) + raw_connection_metadata["nrl.ods-code"] = ods_code + raw_connection_metadata["nrl.app-id"] = nrl_app_id + raw_client_rp_details["developer.app.id"] = nrl_app_id + raw_client_rp_details["developer.app.name"] = nrl_app_id client_rp_details = ClientRpDetails.model_validate(raw_client_rp_details) return ConnectionMetadata.model_validate( diff --git a/layer/nrlf/core/tests/test_authoriser.py b/layer/nrlf/core/tests/test_authoriser.py new file mode 100644 index 000000000..722581212 --- /dev/null +++ b/layer/nrlf/core/tests/test_authoriser.py @@ -0,0 +1,58 @@ +from nrlf.core.authoriser import get_pointer_permissions_v2, parse_permissions_file +from nrlf.core.logger import LogReference, logger +from nrlf.core.request import parse_headers +from nrlf.tests.events import create_headers + + +def test_authoriser_parse_permission_file_with_no_permission_file(): + metadata_result = parse_permissions_file( + connection_metadata=parse_headers(create_headers(ods_code="SomeCode")), + ) + + assert metadata_result == [] + + +def test_authoriser_parse_permission_file_with_permission_file(): + metadata_result = parse_permissions_file( + connection_metadata=parse_headers(create_headers(ods_code="TestCode")), + ) + + assert metadata_result == ["http://snomed.info/sct|736253001"] + + +v2_test_lookup_path = "layer/test_permissions/v2" + + +def test_authoriser_get_v2_permissions_with_pointer_types(mocker): + spy = mocker.spy(logger, "log") + + expected_lookup_key = "producer/ODS123-app-id/ODS123.json" + connection_metadata = parse_headers( + create_headers(ods_code="ODS123", nrl_app_id="ODS123-app-id") + ) + result = get_pointer_permissions_v2( + connection_metadata=connection_metadata, + request_path="/producer/DocumentReference/_search", + lookup_path=v2_test_lookup_path, + ) + + assert result.get("types") == ["http://snomed.info/sct|736253001"] + + spy.assert_called_with(LogReference.V2PERMISSIONS011, key=expected_lookup_key) + + +def test_authoriser_parse_v2_permission_file_with_no_permission_file(mocker): + spy = mocker.spy(logger, "log") + expected_lookup_key = "consumer/NotAnApp/NotFound.json" + + metadata_result = get_pointer_permissions_v2( + connection_metadata=parse_headers( + create_headers(ods_code="NotFound", nrl_app_id="NotAnApp") + ), + request_path="/consumer/_status", + lookup_path=v2_test_lookup_path, + ) + + assert metadata_result == {} + + spy.assert_any_call(LogReference.V2PERMISSIONS011, key=expected_lookup_key) diff --git a/layer/nrlf/core/tests/test_decorators.py b/layer/nrlf/core/tests/test_decorators.py index 9188a0e47..3eff0822e 100644 --- a/layer/nrlf/core/tests/test_decorators.py +++ b/layer/nrlf/core/tests/test_decorators.py @@ -7,7 +7,6 @@ from pydantic import BaseModel from pytest_mock import MockerFixture -from nrlf.core.authoriser import parse_permissions_file from nrlf.core.codes import SpineErrorConcept from nrlf.core.config import Config from nrlf.core.constants import ( @@ -26,7 +25,6 @@ ) from nrlf.core.errors import OperationOutcomeError from nrlf.core.logger import LogReference -from nrlf.core.request import parse_headers from nrlf.core.response import Response from nrlf.tests.events import ( create_headers, @@ -807,20 +805,31 @@ def test_request_load_connection_metadata_with_no_permission_lookup_or_file(): assert expected_metadata.pointer_types == [] -def test_request_parse_permission_file_with_no_permission_file(): - expected_metadata = parse_permissions_file( - connection_metadata=parse_headers(create_headers(ods_code="SomeCode")), - ) +missing_headers = [ + ["nhsd-connection-metadata"], + ["nhsd-connection-metadata", "nhsd-client-rp-details"], + ["nhsd-client-rp-details"], +] - assert expected_metadata == [] +@pytest.mark.parametrize("headers_missing_from_request", missing_headers) +def test_request_load_connection_with_missing_headers_gets_v2_permissions( + headers_missing_from_request, +): + headers = create_headers( + additional_headers={ + "nhsd-end-user-organisation-ods": "Y05868", + "nhsd-nrl-app-id": "Y05868-TestApp-12345678", + } + ) + for header_name in headers_missing_from_request: + headers.pop(header_name) -def test_request_parse_permission_file_with_permission_file(): - expected_metadata = parse_permissions_file( - connection_metadata=parse_headers(create_headers(ods_code="TestCode")), + expected_metadata = load_connection_metadata( + headers=headers, config=Config(), path="/producer/DocumentReference" ) - assert expected_metadata == ["http://snomed.info/sct|736253001"] + assert expected_metadata.pointer_types == [] def test_request_handler_with_custom_repository(mocker: MockerFixture): diff --git a/layer/nrlf/core/tests/test_request.py b/layer/nrlf/core/tests/test_request.py index 9451c9dd5..53ac96448 100644 --- a/layer/nrlf/core/tests/test_request.py +++ b/layer/nrlf/core/tests/test_request.py @@ -3,10 +3,70 @@ import pytest from nrlf.core.errors import OperationOutcomeError, ParseError -from nrlf.core.request import parse_body, parse_headers +from nrlf.core.logger import LogReference, logger +from nrlf.core.request import _fetch_ods_app_id_headers, parse_body, parse_headers from nrlf.producer.fhir.r4.model import DocumentReference from nrlf.tests.data import load_document_reference_data +test_cases = [ + ( + { + "NHSD-end-USER-organISAtion-oDs": "ODS123", + "nhsd-nrl-app-id": "This-is-an-app-id", + }, + "ODS123", + "This-is-an-app-id", + None, + ), + ( + { + "NHSD-end-USER-organISAtion-oDs": "ODS123", + }, + "ODS123", + None, + { + "code": LogReference.HANDLER003b, + "headers_names": ["nhsd-end-user-organisation-ods"], + }, + ), + ( + { + "nHSd-nrL-aPp-Id": "This-is-an-app-id", + }, + None, + "This-is-an-app-id", + { + "code": LogReference.HANDLER003a, + "headers_names": ["nhsd-nrl-app-id"], + }, + ), + ( + {}, + None, + None, + { + "code": LogReference.HANDLER003b, + "headers_names": [], + }, + ), +] + + +@pytest.mark.parametrize( + "headers,expected_ods,expected_app_id,expected_log", test_cases +) +def test_fetch_ods_app_id_headers( + headers, expected_ods, expected_app_id, expected_log, mocker +): + spy = mocker.spy(logger, "log") + ods_code, nrl_app_id = _fetch_ods_app_id_headers(headers) + + assert ods_code == expected_ods + assert nrl_app_id == expected_app_id + + if expected_log: + spy.assert_called_with(**expected_log) + def test_parse_headers_empty_headers(): headers = {} @@ -127,6 +187,36 @@ def test_parse_headers_case_insensitive(): assert metadata.client_rp_details.developer_app_id == "12345" +def test_parse_headers_valid_headers_v2_permissions(): + headers = { + "nhsd-connection-metadata": json.dumps( + { + "nrl.pointer-types": ["pointer_type"], + "nrl.ods-code": "overwrite me", + "nrl.permissions": ["permission1", "permission2"], + "nrl.app-id": "overwrite me", + } + ), + "nhsd-client-rp-details": json.dumps( + { + "developer.app.name": "TestApp", + "developer.app.id": "12345", + } + ), + "nhsd-end-user-organisation-ods": "X26", + "nhsd-nrl-app-id": "X26-TestApp-12345", + } + + metadata = parse_headers(headers, use_v2_permissions=True) + + assert metadata.pointer_types == ["pointer_type"] + assert metadata.ods_code == "X26" + assert metadata.nrl_app_id == "X26-TestApp-12345" + assert metadata.nrl_permissions == ["permission1", "permission2"] + assert metadata.client_rp_details.developer_app_name == "X26-TestApp-12345" + assert metadata.client_rp_details.developer_app_id == "X26-TestApp-12345" + + def test_parse_body_no_model_no_body(): body = None model = None diff --git a/layer/test_permissions/v2/producer/ODS123-app-id/ODS123.json b/layer/test_permissions/v2/producer/ODS123-app-id/ODS123.json new file mode 100644 index 000000000..dd489f013 --- /dev/null +++ b/layer/test_permissions/v2/producer/ODS123-app-id/ODS123.json @@ -0,0 +1,3 @@ +{ + "types": ["http://snomed.info/sct|736253001"] +} diff --git a/terraform/infrastructure/README.md b/terraform/infrastructure/README.md index 65c3a4437..e36b69433 100644 --- a/terraform/infrastructure/README.md +++ b/terraform/infrastructure/README.md @@ -56,7 +56,7 @@ First, build the NRLF artifacts that will be deployed by Terraform: $ make build-artifacts ``` -### Init your local workspace +### Init your local workspace / deploy a feature branch On the first deployment, you will need to initialise and create your workspace. To create a new ephemeral dev workspace, run: @@ -64,6 +64,8 @@ On the first deployment, you will need to initialise and create your workspace. $ make init ``` +### Use an existing workspace / deploy to a persistent environment + If you want to use an existing workspace, or if you want to use the workspace of a persistent environment, do the following: ```shell diff --git a/tests/smoke/scenarios/1dsync_upsert_delete.py b/tests/smoke/scenarios/1dsync_upsert_delete.py index c314dd241..53405e0dd 100644 --- a/tests/smoke/scenarios/1dsync_upsert_delete.py +++ b/tests/smoke/scenarios/1dsync_upsert_delete.py @@ -21,7 +21,7 @@ def producer_client_1dsync( return ProducerTestClient(config=client_config) -def test_1dsync_upsert_delete( +def test_smoke_1dsync_upsert_delete( producer_client_1dsync: ProducerTestClient, smoke_test_parameters: SmokeTestParameters, test_nhs_numbers: list[str], diff --git a/tests/smoke/scenarios/api_capability_statement_lookup.py b/tests/smoke/scenarios/api_capability_statement_lookup.py index 71959df30..636b9b0af 100644 --- a/tests/smoke/scenarios/api_capability_statement_lookup.py +++ b/tests/smoke/scenarios/api_capability_statement_lookup.py @@ -1,7 +1,7 @@ from tests.utilities.api_clients import ConsumerTestClient, ProducerTestClient -def test_read_api_capability_statements( +def test_smoke_read_api_capability_statements( consumer_client: ConsumerTestClient, producer_client: ProducerTestClient ): """ diff --git a/tests/smoke/scenarios/consumer_search_read.py b/tests/smoke/scenarios/consumer_search_read.py index 969e37b42..edbd41c88 100644 --- a/tests/smoke/scenarios/consumer_search_read.py +++ b/tests/smoke/scenarios/consumer_search_read.py @@ -36,7 +36,9 @@ def test_data( producer_client.delete(test_pointer.id) -def test_consumer_search_read(consumer_client: ConsumerTestClient, test_data: dict): +def test_smoke_consumer_search_read( + consumer_client: ConsumerTestClient, test_data: dict +): """ Smoke test scenario for a consumer search and read behaviour """ diff --git a/tests/smoke/scenarios/producer_crud.py b/tests/smoke/scenarios/producer_crud.py index 8f4e5e578..a5e817768 100644 --- a/tests/smoke/scenarios/producer_crud.py +++ b/tests/smoke/scenarios/producer_crud.py @@ -3,7 +3,7 @@ from tests.utilities.api_clients import ProducerTestClient -def test_producer_crud( +def test_smoke_producer_crud( producer_client: ProducerTestClient, smoke_test_parameters: SmokeTestParameters, test_nhs_numbers: list[str], diff --git a/tests/smoke/scenarios/producer_search_read.py b/tests/smoke/scenarios/producer_search_read.py index b38b52fdc..d3b496c41 100644 --- a/tests/smoke/scenarios/producer_search_read.py +++ b/tests/smoke/scenarios/producer_search_read.py @@ -36,7 +36,9 @@ def test_data( producer_client.delete(test_pointer.id) -def test_producer_search_read(producer_client: ProducerTestClient, test_data: dict): +def test_smoke_producer_search_read( + producer_client: ProducerTestClient, test_data: dict +): """ Smoke test scenario for a producer search and read behaviour """