From 257de74b68b7ae7d9c643fc819e3cd21ac1d07ce Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Thu, 13 Feb 2025 08:43:47 +0000 Subject: [PATCH 1/9] [NRL-1290] Extend model to support structured content format --- api/consumer/swagger.yaml | 2 ++ api/producer/swagger.yaml | 2 ++ layer/nrlf/consumer/fhir/r4/model.py | 14 +++++++++++--- layer/nrlf/producer/fhir/r4/model.py | 14 +++++++++++--- layer/nrlf/producer/fhir/r4/strict_model.py | 14 +++++++++++--- 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 5d016c4e7..5140b5604 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1250,12 +1250,14 @@ components: enum: - "urn:nhs-ic:record-contact" - "urn:nhs-ic:unstructured" + - "urn:nhs-ic:structured" description: The code representing the format of the document. display: type: string enum: - "Contact details (HTTP Unsecured)" - "Unstructured Document" + - "Structured Document" description: The display text for the code. required: - system diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index 76d1f4284..5eb9066fd 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1868,12 +1868,14 @@ components: enum: - "urn:nhs-ic:record-contact" - "urn:nhs-ic:unstructured" + - "urn:nhs-ic:structured" description: The code representing the format of the document. display: type: string enum: - "Contact details (HTTP Unsecured)" - "Unstructured Document" + - "Structured Document" description: The display text for the code. required: - system diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 5f9b911a6..ddbf13568 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-02-07T14:10:39+00:00 +# timestamp: 2025-02-13T08:42:14+00:00 from __future__ import annotations @@ -246,11 +246,19 @@ class NRLFormatCode(Coding): Field(description="The system URL for the NRLF Format Code."), ] code: Annotated[ - Literal["urn:nhs-ic:record-contact", "urn:nhs-ic:unstructured"], + Literal[ + "urn:nhs-ic:record-contact", + "urn:nhs-ic:unstructured", + "urn:nhs-ic:structured", + ], Field(description="The code representing the format of the document."), ] display: Annotated[ - Literal["Contact details (HTTP Unsecured)", "Unstructured Document"], + Literal[ + "Contact details (HTTP Unsecured)", + "Unstructured Document", + "Structured Document", + ], Field(description="The display text for the code."), ] diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index 715a7e12e..7d4869f11 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-02-07T14:10:35+00:00 +# timestamp: 2025-02-13T08:42:12+00:00 from __future__ import annotations @@ -290,11 +290,19 @@ class NRLFormatCode(Coding): Field(description="The system URL for the NRLF Format Code."), ] code: Annotated[ - Literal["urn:nhs-ic:record-contact", "urn:nhs-ic:unstructured"], + Literal[ + "urn:nhs-ic:record-contact", + "urn:nhs-ic:unstructured", + "urn:nhs-ic:structured", + ], Field(description="The code representing the format of the document."), ] display: Annotated[ - Literal["Contact details (HTTP Unsecured)", "Unstructured Document"], + Literal[ + "Contact details (HTTP Unsecured)", + "Unstructured Document", + "Structured Document", + ], Field(description="The display text for the code."), ] diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index d7849f154..5fc7878c7 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-02-07T14:10:37+00:00 +# timestamp: 2025-02-13T08:42:13+00:00 from __future__ import annotations @@ -261,11 +261,19 @@ class NRLFormatCode(Coding): Field(description="The system URL for the NRLF Format Code."), ] code: Annotated[ - Literal["urn:nhs-ic:record-contact", "urn:nhs-ic:unstructured"], + Literal[ + "urn:nhs-ic:record-contact", + "urn:nhs-ic:unstructured", + "urn:nhs-ic:structured", + ], Field(description="The code representing the format of the document."), ] display: Annotated[ - Literal["Contact details (HTTP Unsecured)", "Unstructured Document"], + Literal[ + "Contact details (HTTP Unsecured)", + "Unstructured Document", + "Structured Document", + ], Field(description="The display text for the code."), ] From 65915882ec26f803b4b77cf93480009c1e61d41d Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Thu, 13 Feb 2025 09:13:42 +0000 Subject: [PATCH 2/9] [NRL-1290] Add bars pointer type and category. Adapt pointer validators to allow bars pointers for prototype --- layer/nrlf/core/constants.py | 12 ++++++++++++ layer/nrlf/core/tests/test_validators.py | 3 +++ layer/nrlf/core/validators.py | 16 +++++++++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/layer/nrlf/core/constants.py b/layer/nrlf/core/constants.py index d7b94f366..6f1d6d5cd 100644 --- a/layer/nrlf/core/constants.py +++ b/layer/nrlf/core/constants.py @@ -66,6 +66,7 @@ class PointerTypes(Enum): PERSONALISED_CARE_AND_SUPPORT_PLAN = "http://snomed.info/sct|2181441000000107" MRA_UPPER_LIMB_ARTERY = "https://nicip.nhs.uk|MAULR" MRI_AXILLA_BOTH = "https://nicip.nhs.uk|MAXIB" + APPOINTMENT = "http://snomed.info/sct|749001000000101" @staticmethod def list(): @@ -84,6 +85,7 @@ class Categories(Enum): CLINICAL_NOTE = "http://snomed.info/sct|823651000000106" DIAGNOSTIC_STUDIES_REPORT = "http://snomed.info/sct|721981007" DIAGNOSTIC_PROCEDURE = "http://snomed.info/sct|103693007" + RECORD_ARTIFACT = "http://snomed.info/sct|419891008" @staticmethod def list(): @@ -112,6 +114,9 @@ def coding_value(self): Categories.DIAGNOSTIC_PROCEDURE.value: { "display": "Diagnostic procedure", }, + Categories.RECORD_ARTIFACT.value: { + "display": "Record artifact (record artifact)", + }, } TYPE_ATTRIBUTES = { @@ -157,6 +162,9 @@ def coding_value(self): PointerTypes.MRI_AXILLA_BOTH.value: { "display": "MRI Axilla Both", }, + PointerTypes.APPOINTMENT.value: { + "display": "Appointment (record artifact)", + }, } TYPE_CATEGORIES = { @@ -182,6 +190,9 @@ def coding_value(self): # Imaging PointerTypes.MRA_UPPER_LIMB_ARTERY.value: Categories.DIAGNOSTIC_STUDIES_REPORT.value, PointerTypes.MRI_AXILLA_BOTH.value: Categories.DIAGNOSTIC_PROCEDURE.value, + # + # Bars + PointerTypes.APPOINTMENT.value: Categories.RECORD_ARTIFACT.value, } PRACTICE_SETTING_VALUE_SET_URL = ( @@ -653,6 +664,7 @@ def coding_value(self): "24291000087104": "Geriatric chronic pain management service", "1323501000000109": "Special care dentistry service", "1423561000000102": "Acute oncology service", + "394802001": "General medicine (qualifier value)", } diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 8c5c36e0f..e101350ff 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -7,6 +7,7 @@ ODS_SYSTEM, TYPE_ATTRIBUTES, TYPE_CATEGORIES, + Categories, PointerTypes, ) from nrlf.core.errors import ParseError @@ -430,6 +431,7 @@ def test_validate_category_too_many_category(): [ (category_str.split("|")[1], display_dict["display"]) for category_str, display_dict in CATEGORY_ATTRIBUTES.items() + if category_str != Categories.RECORD_ARTIFACT.value ], ) def test_validate_category_coding_display_mismatch( @@ -639,6 +641,7 @@ def test_validate_type_coding_invalid_system(): [ (type_str, display_dict["display"]) for type_str, display_dict in TYPE_ATTRIBUTES.items() + if type_str != PointerTypes.APPOINTMENT.value ], ) def test_validate_type_coding_display_mismatch(type_str: str, display: str): diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 94c086afa..905cb6020 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -368,6 +368,10 @@ def _validate_type(self, model: DocumentReference): ) return + # Bypass display validation for bars appointments + if type_id == PointerTypes.APPOINTMENT.value: + return + type_attributes = TYPE_ATTRIBUTES.get(type_id, {}) if coding.display != type_attributes.get("display"): self.result.add_error( @@ -423,6 +427,10 @@ def _validate_category(self, model: DocumentReference): ) return + # Bypass display validation for bars record artifacts + if category_id == Categories.RECORD_ARTIFACT.value: + return + category_attributes = CATEGORY_ATTRIBUTES.get(category_id, {}) if coding.display != category_attributes.get("display"): self.result.add_error( @@ -623,7 +631,13 @@ def _validate_content(self, model: DocumentReference): } for i, content in enumerate(model.content): - if content.attachment.contentType not in ["application/pdf", "text/html"]: + if content.attachment.contentType not in [ + "application/pdf", + "text/html", + "application/fhir+json", + "application/json", + "application/json+fhir", + ]: self.result.add_error( issue_code="value", error_code="INVALID_RESOURCE", From 29a2a099f0d586534b86efa73e7443c7344bf844 Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Thu, 13 Feb 2025 11:19:28 +0000 Subject: [PATCH 3/9] [NRL-1290] Add bars pointer samples --- .../X26_APPOINTMENT_01_BARS_Feb25.json | 102 +++++++++++++++++ .../X26_APPOINTMENT_02_BARS_Feb25.json | 106 ++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 tests/data/samples/X26_APPOINTMENT_01_BARS_Feb25.json create mode 100644 tests/data/samples/X26_APPOINTMENT_02_BARS_Feb25.json diff --git a/tests/data/samples/X26_APPOINTMENT_01_BARS_Feb25.json b/tests/data/samples/X26_APPOINTMENT_01_BARS_Feb25.json new file mode 100644 index 000000000..0c0bdcc4d --- /dev/null +++ b/tests/data/samples/X26_APPOINTMENT_01_BARS_Feb25.json @@ -0,0 +1,102 @@ +{ + "resourceType": "DocumentReference", + "id": "X26-90c6cafb-18ee-4b0f-8109-7ba1e6d5274e", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/BaRS-Identifier", + "value": "GL0-DZOqD39" + }, + { + "system": "https://fhir.nhs.uk/Id/dos-service-id", + "value": "2000072491" + }, + { + "system": "https://fhir.nhs.uk/id/product-id", + "value": "P.GH7-4TY" + } + ], + "status": "current", + "type": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "749001000000101", + "display": "Appointment (record artifact)" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "419891008", + "display": "Record artifact (record artifact)" + } + ] + } + ], + "subject": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9693893123" + } + }, + "date": "2025-02-05T08:25:13.136693+00:00", + "author": [ + { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "RGT" + } + } + ], + "custodian": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "RGT" + } + }, + "content": [ + { + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ], + "attachment": { + "contentType": "application/fhir+json", + "url": "https://server.fire.ly/r4/Appointment/GL0-DZOqD39" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:structured", + "display": "Structured Document" + } + } + ], + "context": { + "period": { + "start": "2025-02-01T06:45:00+00:00", + "end": "2025-02-01T07:00:00+00:00" + }, + "practiceSetting": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "394802001", + "display": "General medicine (qualifier value)" + } + ] + } + } +} diff --git a/tests/data/samples/X26_APPOINTMENT_02_BARS_Feb25.json b/tests/data/samples/X26_APPOINTMENT_02_BARS_Feb25.json new file mode 100644 index 000000000..c4c2f4c14 --- /dev/null +++ b/tests/data/samples/X26_APPOINTMENT_02_BARS_Feb25.json @@ -0,0 +1,106 @@ +{ + "resourceType": "DocumentReference", + "id": "X26-90c6cafb-18ee-4b0f-8109-7ba1e6d5274e", + "masterIdentifier": { + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:27e2b1c8-ecd8-48f8-9958-8e614cc7ad73" + }, + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/BaRS-Identifier", + "value": "4a3836f5-2d42-4d3e-87c1-680173b7fa5c" + }, + { + "system": "https://fhir.nhs.uk/Id/dos-service-id", + "value": "matthewbrown" + }, + { + "system": "https://fhir.nhs.uk/id/product-id", + "value": "6a5fc9f4-4af4-4819-b5d1-1339d9b64295" + } + ], + "status": "current", + "type": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "749001000000101", + "display": "Appointment" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "419891008", + "display": "Record Artifact" + } + ] + } + ], + "subject": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9876543210" + } + }, + "author": [ + { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "X26" + } + } + ], + "custodian": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "X26" + } + }, + "content": [ + { + "attachment": { + "contentType": "application/fhir+json", + "language": "en-UK", + "url": "https://bars-int-x26.tsassolarch.thirdparty.nhs.uk/barspoc/FHIR/R4/Appointment/4a3836f5-2d42-4d3e-87c1-680173b7fa5c" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:structured", + "display": "Structured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "dynamic", + "display": "Dynamic" + } + ] + } + } + ] + } + ], + "context": { + "period": { + "start": "2025-01-15T09:50:00Z", + "end": "2025-01-15T10:00:00Z" + }, + "practiceSetting": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "394802001", + "display": "General medicine (qualifier value)" + } + ] + } + } +} From 3fc609334bd65e1d234cf5650104c0687368444b Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Thu, 13 Feb 2025 14:21:58 +0000 Subject: [PATCH 4/9] [NRL-1290] Allow bars proxy to create and update appointments for any org --- .../create_document_reference.py | 5 +++++ .../update_document_reference.py | 13 ++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/api/producer/createDocumentReference/create_document_reference.py b/api/producer/createDocumentReference/create_document_reference.py index 020f84d80..f9321072a 100644 --- a/api/producer/createDocumentReference/create_document_reference.py +++ b/api/producer/createDocumentReference/create_document_reference.py @@ -64,6 +64,11 @@ def _check_permissions( """ Check the requester has permissions to create the DocumentReference """ + + # Allow BARS proxy to create a document reference for any organisation + if metadata.ods_code == "V4TOL" and core_model.type in metadata.pointer_types: + return + custodian_parts = tuple( filter(None, (core_model.custodian, core_model.custodian_suffix)) ) diff --git a/api/producer/updateDocumentReference/update_document_reference.py b/api/producer/updateDocumentReference/update_document_reference.py index 1f7400ee7..4910b4727 100644 --- a/api/producer/updateDocumentReference/update_document_reference.py +++ b/api/producer/updateDocumentReference/update_document_reference.py @@ -3,6 +3,7 @@ from pydantic import ValidationError from nrlf.core.codes import SpineErrorConcept +from nrlf.core.constants import PointerTypes from nrlf.core.decorators import DocumentPointerRepository, request_handler from nrlf.core.dynamodb.model import DocumentPointer from nrlf.core.errors import OperationOutcomeError @@ -61,7 +62,17 @@ def handler( core_model = DocumentPointer.from_document_reference(document_reference) - if metadata.ods_code_parts != tuple(core_model.producer_id.split("|")): + if ( + metadata.ods_code == "V4TOL" + and core_model.type.coding[0].code == PointerTypes.APPOINTMENT.coding_value() + ): + # If bars app - don't validate the ods code against the pointer + logger.log( + LogReference.PROUPDATE002, + msg="Allowing bars to update pointer", + pointer_id=core_model.id, + ) + elif metadata.ods_code_parts != tuple(core_model.producer_id.split("|")): logger.log( LogReference.PROUPDATE004, metadata_ods_code_parts=metadata.ods_code_parts, From ce26efc4fedf9424f0b6ce2f8d9675daaac91bb5 Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Thu, 13 Feb 2025 15:12:18 +0000 Subject: [PATCH 5/9] [NRL-1290] Disable setting of custodian suffix from ods-code-extension metadata --- layer/nrlf/core/authoriser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/layer/nrlf/core/authoriser.py b/layer/nrlf/core/authoriser.py index 137048abb..30d4dc8d7 100644 --- a/layer/nrlf/core/authoriser.py +++ b/layer/nrlf/core/authoriser.py @@ -19,10 +19,10 @@ def get_pointer_types( ods_code = connection_metadata.ods_code ods_code_extension = connection_metadata.ods_code_extension - if ods_code_extension: - key = f"{app_id}/{ods_code}.{ods_code_extension}.json" - else: - key = f"{app_id}/{ods_code}.json" + # if ods_code_extension: + # key = f"{app_id}/{ods_code}.{ods_code_extension}.json" + # else: + key = f"{app_id}/{ods_code}.json" logger.log(LogReference.S3PERMISSIONS001, bucket=config.AUTH_STORE, key=key) s3_client = get_s3_client() From 0de6492760d57929e5bd863d6144a6474e174107 Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Thu, 13 Feb 2025 16:22:33 +0000 Subject: [PATCH 6/9] [NRL-1290] Allow both V4TOL and V4T0L as bars ods code. Add new proxy permission for create --- .../create_document_reference.py | 23 ++++++++++++++++--- .../update_document_reference.py | 2 +- layer/nrlf/core/constants.py | 5 ++++ layer/nrlf/core/model.py | 2 ++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/api/producer/createDocumentReference/create_document_reference.py b/api/producer/createDocumentReference/create_document_reference.py index f9321072a..fb7309719 100644 --- a/api/producer/createDocumentReference/create_document_reference.py +++ b/api/producer/createDocumentReference/create_document_reference.py @@ -2,8 +2,10 @@ from nrlf.core.codes import SpineErrorConcept from nrlf.core.constants import ( + PERMISSION_ALLOW_PROXY_ODS_CODES, PERMISSION_AUDIT_DATES_FROM_PAYLOAD, PERMISSION_SUPERSEDE_IGNORE_DELETE_FAIL, + PointerTypes, ) from nrlf.core.decorators import request_handler from nrlf.core.dynamodb.repository import DocumentPointer, DocumentPointerRepository @@ -65,14 +67,29 @@ def _check_permissions( Check the requester has permissions to create the DocumentReference """ - # Allow BARS proxy to create a document reference for any organisation - if metadata.ods_code == "V4TOL" and core_model.type in metadata.pointer_types: + # Allow BARS proxy to create an appointment document reference for any organisation + if ( + metadata.ods_code in ["V4TOL", "V4T0L"] + and core_model.type.coding[0].code == PointerTypes.APPOINTMENT.coding_value() + ): return + allow_ods_code_proxying = False + if PERMISSION_ALLOW_PROXY_ODS_CODES in metadata.nrl_permissions: + if metadata.ods_code == metadata.nrl_proxy_ods_code: + allow_ods_code_proxying = True + else: + logger.log( + LogReference.PROCREATE003, + ods_code=metadata.ods_code, + proxy_ods_code=metadata.nrl_proxy_ods_code, + warning="Unable to allow ods code proxying as the ods code does not match the configured proxy ods code", + ) + custodian_parts = tuple( filter(None, (core_model.custodian, core_model.custodian_suffix)) ) - if metadata.ods_code_parts != custodian_parts: + if not allow_ods_code_proxying and metadata.ods_code_parts != custodian_parts: logger.log( LogReference.PROCREATE004, ods_code_parts=metadata.ods_code_parts, diff --git a/api/producer/updateDocumentReference/update_document_reference.py b/api/producer/updateDocumentReference/update_document_reference.py index 4910b4727..728917308 100644 --- a/api/producer/updateDocumentReference/update_document_reference.py +++ b/api/producer/updateDocumentReference/update_document_reference.py @@ -63,7 +63,7 @@ def handler( core_model = DocumentPointer.from_document_reference(document_reference) if ( - metadata.ods_code == "V4TOL" + metadata.ods_code in ["V4TOL", "V4T0L"] and core_model.type.coding[0].code == PointerTypes.APPOINTMENT.coding_value() ): # If bars app - don't validate the ods code against the pointer diff --git a/layer/nrlf/core/constants.py b/layer/nrlf/core/constants.py index 6f1d6d5cd..4a4b3fb9c 100644 --- a/layer/nrlf/core/constants.py +++ b/layer/nrlf/core/constants.py @@ -35,12 +35,17 @@ class Source(Enum): "incorporates", "summarizes", } + CLIENT_RP_DETAILS = "nhsd-client-rp-details" CONNECTION_METADATA = "nhsd-connection-metadata" PERMISSION_AUDIT_DATES_FROM_PAYLOAD = "audit-dates-from-payload" PERMISSION_SUPERSEDE_IGNORE_DELETE_FAIL = "supersede-ignore-delete-fail" PERMISSION_ALLOW_ALL_POINTER_TYPES = "allow-all-pointer-types" +# Bars prototype - Allow a application to proxy as any organisation +METADATA_PROXY_ODS_CODE = "nrl.proxy-ods-code" +PERMISSION_ALLOW_PROXY_ODS_CODES = "allow-proxy-ods-codes" + NHSD_REQUEST_ID_HEADER = "NHSD-Request-Id" NHSD_CORRELATION_ID_HEADER = "NHSD-Correlation-Id" diff --git a/layer/nrlf/core/model.py b/layer/nrlf/core/model.py index 05b3e5245..ed1dce5d2 100644 --- a/layer/nrlf/core/model.py +++ b/layer/nrlf/core/model.py @@ -5,6 +5,7 @@ import nrlf.consumer.fhir.r4.model as consumer_model import nrlf.producer.fhir.r4.model as producer_model +from nrlf.core.constants import METADATA_PROXY_ODS_CODE class _NhsNumberMixin: @@ -56,6 +57,7 @@ class ConnectionMetadata(BaseModel): ods_code_extension: str | None = Field(alias="nrl.ods-code-extension", default=None) nrl_permissions: list[str] = Field(alias="nrl.permissions", default_factory=list) nrl_app_id: str = Field(alias="nrl.app-id") + nrl_proxy_ods_code: str | None = Field(alias=METADATA_PROXY_ODS_CODE, default=None) is_test_event: bool = Field(alias="nrl.test-event", default=False) client_rp_details: ClientRpDetails From 93344836f4017b1e0d97c501a55ef06c90ef35f9 Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Thu, 13 Feb 2025 16:40:36 +0000 Subject: [PATCH 7/9] [NRL-1290] Fix typo in use of DB model for bars checks --- .../createDocumentReference/create_document_reference.py | 2 +- .../updateDocumentReference/update_document_reference.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/producer/createDocumentReference/create_document_reference.py b/api/producer/createDocumentReference/create_document_reference.py index fb7309719..1b319054a 100644 --- a/api/producer/createDocumentReference/create_document_reference.py +++ b/api/producer/createDocumentReference/create_document_reference.py @@ -70,7 +70,7 @@ def _check_permissions( # Allow BARS proxy to create an appointment document reference for any organisation if ( metadata.ods_code in ["V4TOL", "V4T0L"] - and core_model.type.coding[0].code == PointerTypes.APPOINTMENT.coding_value() + and core_model.type == PointerTypes.APPOINTMENT.value ): return diff --git a/api/producer/updateDocumentReference/update_document_reference.py b/api/producer/updateDocumentReference/update_document_reference.py index 728917308..a78a55125 100644 --- a/api/producer/updateDocumentReference/update_document_reference.py +++ b/api/producer/updateDocumentReference/update_document_reference.py @@ -64,7 +64,7 @@ def handler( if ( metadata.ods_code in ["V4TOL", "V4T0L"] - and core_model.type.coding[0].code == PointerTypes.APPOINTMENT.coding_value() + and core_model.type == PointerTypes.APPOINTMENT.value ): # If bars app - don't validate the ods code against the pointer logger.log( From fc892c5e6137f357181e0f67e07ae90ecc568810 Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Fri, 14 Feb 2025 09:40:45 +0000 Subject: [PATCH 8/9] [NRL-1290] Comment out the custodian suffix integ tests as suffixes are disabled in the prototype --- .../readDocumentReference-failure.feature | 82 +++++++++---------- .../readDocumentReference-success.feature | 43 +++++----- 2 files changed, 63 insertions(+), 62 deletions(-) diff --git a/tests/features/producer/readDocumentReference-failure.feature b/tests/features/producer/readDocumentReference-failure.feature index ff2cce7d0..4a35d46f9 100644 --- a/tests/features/producer/readDocumentReference-failure.feature +++ b/tests/features/producer/readDocumentReference-failure.feature @@ -100,44 +100,44 @@ Feature: Producer - readDocumentReference - Failure Scenarios } """ - Scenario: Read document reference by ID - custodian suffix mismatch - Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API - And the organisation 'RX898.001' is authorised to access pointer types: - | system | value | - | http://snomed.info/sct | 736253002 | - And the organisation 'RX898.002' is authorised to access pointer types: - | system | value | - | http://snomed.info/sct | 736253002 | - And a DocumentReference resource exists with values: - | property | value | - | id | RX898.001-1234567890-CustSuffixMismatch | - | subject | 9999999999 | - | status | current | - | type | 736253002 | - | category | 734163000 | - | contentType | application/pdf | - | url | https://example.org/my-doc.pdf | - | custodian | RX898.001 | - | author | X26 | - When producer 'RX898.001' reads a DocumentReference with ID 'RX898.001-1234567890-CustSuffixMismatch' - Then the response status code is 200 - When producer 'RX898.002' reads a DocumentReference with ID 'RX898.001-1234567890-CustSuffixMismatch' - Then the response status code is 403 - And the response is an OperationOutcome with 1 issue - And the OperationOutcome contains the issue: - """ - { - "severity": "error", - "code": "forbidden", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "AUTHOR_CREDENTIALS_ERROR", - "display": "Author credentials error" - } - ] - }, - "diagnostics": "The requested DocumentReference cannot be read because it belongs to another organisation" - } - """ +# Scenario: Read document reference by ID - custodian suffix mismatch +# Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API +# And the organisation 'RX898.001' is authorised to access pointer types: +# | system | value | +# | http://snomed.info/sct | 736253002 | +# And the organisation 'RX898.002' is authorised to access pointer types: +# | system | value | +# | http://snomed.info/sct | 736253002 | +# And a DocumentReference resource exists with values: +# | property | value | +# | id | RX898.001-1234567890-CustSuffixMismatch | +# | subject | 9999999999 | +# | status | current | +# | type | 736253002 | +# | category | 734163000 | +# | contentType | application/pdf | +# | url | https://example.org/my-doc.pdf | +# | custodian | RX898.001 | +# | author | X26 | +# When producer 'RX898.001' reads a DocumentReference with ID 'RX898.001-1234567890-CustSuffixMismatch' +# Then the response status code is 200 +# When producer 'RX898.002' reads a DocumentReference with ID 'RX898.001-1234567890-CustSuffixMismatch' +# Then the response status code is 403 +# And the response is an OperationOutcome with 1 issue +# And the OperationOutcome contains the issue: +# """ +# { +# "severity": "error", +# "code": "forbidden", +# "details": { +# "coding": [ +# { +# "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", +# "code": "AUTHOR_CREDENTIALS_ERROR", +# "display": "Author credentials error" +# } +# ] +# }, +# "diagnostics": "The requested DocumentReference cannot be read because it belongs to another organisation" +# } +# """ diff --git a/tests/features/producer/readDocumentReference-success.feature b/tests/features/producer/readDocumentReference-success.feature index 9e4c2f8d7..d818868cb 100644 --- a/tests/features/producer/readDocumentReference-success.feature +++ b/tests/features/producer/readDocumentReference-success.feature @@ -107,24 +107,25 @@ Feature: Producer - readDocumentReference - Success Scenarios } """ - Scenario: Read document reference by ID - custodian suffix - Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API - And the organisation 'RX898.001' is authorised to access pointer types: - | system | value | - | http://snomed.info/sct | 736253002 | - And the organisation 'RX898.002' is authorised to access pointer types: - | system | value | - | http://snomed.info/sct | 736253002 | - And a DocumentReference resource exists with values: - | property | value | - | id | RX898.001-1234567890-ReadDocRefCustSuffix | - | subject | 9999999999 | - | status | current | - | type | 736253002 | - | category | 734163000 | - | contentType | application/pdf | - | url | https://example.org/my-doc.pdf | - | custodian | RX898.001 | - | author | HAR1 | - When producer 'RX898.001' reads a DocumentReference with ID 'RX898.001-1234567890-ReadDocRefCustSuffix' - Then the response status code is 200 +# Scenario: Read document reference by ID - custodian suffix +# Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API +# And the organisation 'RX898.001' is authorised to access pointer types: +# | system | value | +# | http://snomed.info/sct | 736253002 | +# And the organisation 'RX898.002' is authorised to access pointer types: +# | system | value | +# | http://snomed.info/sct | 736253002 | +# And a DocumentReference resource exists with values: +# | property | value | +# | id | RX898.001-1234567890-ReadDocRefCustSuffix | +# | subject | 9999999999 | +# | status | current | +# | type | 736253002 | +# | category | 734163000 | +# | contentType | application/pdf | +# | url | https://example.org/my-doc.pdf | +# | custodian | RX898.001 | +# | author | HAR1 | +# When producer 'RX898.001' reads a DocumentReference with ID 'RX898.001-1234567890-ReadDocRefCustSuffix' +# Then the response status code is 200 +# From 5d024300ddaf47e8f50327a2b96e200fbe8695a3 Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Fri, 14 Feb 2025 14:22:07 +0000 Subject: [PATCH 9/9] [NRL-1290] Fix typo in log message for allow bars updates --- .../updateDocumentReference/update_document_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/producer/updateDocumentReference/update_document_reference.py b/api/producer/updateDocumentReference/update_document_reference.py index a78a55125..a7f9f8829 100644 --- a/api/producer/updateDocumentReference/update_document_reference.py +++ b/api/producer/updateDocumentReference/update_document_reference.py @@ -69,7 +69,7 @@ def handler( # If bars app - don't validate the ods code against the pointer logger.log( LogReference.PROUPDATE002, - msg="Allowing bars to update pointer", + allow_rule="Allowing bars to update pointer", pointer_id=core_model.id, ) elif metadata.ods_code_parts != tuple(core_model.producer_id.split("|")):