Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e0f1027
NRL-1949 Add consumer & producer objects to auth store
sandyforresternhs Feb 26, 2026
1a3e838
Merge branch 'develop' into feature/SAFO6-NRL-1949-assign-pointer-typ…
sandyforresternhs Mar 3, 2026
7eeb57f
NRL-1949 Add V2 consumer pointer type feature tests
sandyforresternhs Mar 5, 2026
ac060ab
NRL-1949 Add todo
sandyforresternhs Mar 5, 2026
e5d4edd
NRL-1949 Add producer v2 WIP
sandyforresternhs Mar 5, 2026
db65883
NRL-1949 V2 Producer tests for pointer type
sandyforresternhs Mar 6, 2026
dddea42
NRL-1949 Tidy up client classes
sandyforresternhs Mar 6, 2026
833e404
NRL-1949 Tidy up write permissions
sandyforresternhs Mar 6, 2026
791f3a3
NRL-1949 Amend comments
sandyforresternhs Mar 6, 2026
2762827
Merge branch 'develop' into feature/SAFO6-NRL-1949-assign-pointer-typ…
sandyforresternhs Mar 6, 2026
2caf29a
NRL-1949 Address pr comments
sandyforresternhs Mar 9, 2026
c6ee2ea
Merge branch 'develop' into feature/SAFO6-NRL-1949-assign-pointer-typ…
sandyforresternhs Mar 9, 2026
ef4922b
Merge branch 'develop' into feature/SAFO6-NRL-1949-assign-pointer-typ…
sandyforresternhs Mar 9, 2026
5dcd0de
Merge branch 'develop' into feature/SAFO6-NRL-1949-assign-pointer-typ…
sandyforresternhs Mar 10, 2026
81ada46
NRL-1949 Add v2 perm policy to metadata & update producer endpoints
sandyforresternhs Mar 11, 2026
b128293
NRL-1949 Add Update consumer endpoints with v2 permission policy
sandyforresternhs Mar 11, 2026
b8820d0
NRL-1986 Add allow_all_types consumer test and v2 enums
sandyforresternhs Mar 11, 2026
6eb0ae6
NRL-1986 Add allow_all_types producer feature test
sandyforresternhs Mar 12, 2026
7ad3bb9
NRL-1986 Address PR comments
sandyforresternhs Mar 12, 2026
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
10 changes: 8 additions & 2 deletions api/consumer/readDocumentReference/read_document_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,18 @@ def handler(
diagnostics="The requested DocumentReference could not be found"
)

if result.type not in metadata.pointer_types:
allowed_types = (
metadata.nrl_permissions_policy.types
if metadata.nrl_permissions_policy
else metadata.pointer_types
)

if result.type not in allowed_types:
logger.log(
LogReference.CONREAD002,
ods_code=metadata.ods_code,
type=result.type,
pointer_types=metadata.pointer_types,
pointer_types=allowed_types,
)
return SpineErrorResponse.ACCESS_DENIED(
diagnostics="The requested DocumentReference is not of a type that this organisation is allowed to access"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import json
from unittest.mock import patch

from moto import mock_aws

from api.consumer.readDocumentReference.read_document_reference import handler
from nrlf.core.constants import CLIENT_RP_DETAILS, V2Headers
from nrlf.core.dynamodb.repository import DocumentPointer, DocumentPointerRepository
from nrlf.tests.data import load_document_reference
from nrlf.tests.dynamodb import mock_repository
Expand Down Expand Up @@ -41,6 +43,47 @@ def test_read_document_reference_happy_path(
assert parsed_body == doc_ref.model_dump(exclude_none=True)


@mock_aws
@mock_repository
@patch("nrlf.core.decorators.get_pointer_permissions_v2")
def test_read_document_reference_happy_path_v2(
get_pointer_permissions_mock, repository: DocumentPointerRepository
):
doc_ref = load_document_reference("Y05868-736253002-Valid")
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
repository.create(doc_pointer)

v2_headers = create_headers(
additional_headers={
V2Headers.NHSD_END_USER_ORGANISATION_ODS: "Y05868",
V2Headers.NHSD_NRL_APP_ID: "Y05868-TestApp-12345678",
}
)
v2_headers.pop(CLIENT_RP_DETAILS)

get_pointer_permissions_mock.return_value = {
"access_controls": [],
"types": ["http://snomed.info/sct|736253002"],
}

event = create_test_api_gateway_event(
headers=v2_headers,
path_parameters={"id": doc_pointer.id},
)

result = handler(event, create_mock_context())
body = result.pop("body")

assert result == {
"statusCode": "200",
"headers": default_response_headers(),
"isBase64Encoded": False,
}

parsed_body = json.loads(body)
assert parsed_body == doc_ref.model_dump(exclude_none=True)


@mock_aws
@mock_repository
def test_read_document_reference_not_found(repository: DocumentPointerRepository):
Expand Down Expand Up @@ -159,6 +202,65 @@ def test_read_document_reference_unauthorised_for_type(
}


@mock_aws
@mock_repository
@patch("nrlf.core.decorators.get_pointer_permissions_v2")
def test_read_document_reference_unauthorised_for_type_v2(
get_pointer_permissions_mock, repository: DocumentPointerRepository
):
doc_ref = load_document_reference("Y05868-736253002-Valid")
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
repository.create(doc_pointer)

headers = create_headers(
additional_headers={
V2Headers.NHSD_END_USER_ORGANISATION_ODS: "Y05868",
V2Headers.NHSD_NRL_APP_ID: "Y05868-TestApp-12345678",
}
)
headers.pop("nhsd-client-rp-details")

get_pointer_permissions_mock.return_value = {
"access_controls": [],
"types": ["http://snomed.info/sct|736373009"],
}

event = create_test_api_gateway_event(
headers=headers,
path_parameters={"id": doc_pointer.id},
)

result = handler(event, create_mock_context())
body = result.pop("body")

assert result == {
"statusCode": "403",
"headers": default_response_headers(),
"isBase64Encoded": False,
}

parsed_body = json.loads(body)
assert parsed_body == {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "forbidden",
"details": {
"coding": [
{
"code": "ACCESS DENIED",
"display": "Access has been denied to process this request",
"system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode",
}
]
},
"diagnostics": "The requested DocumentReference is not of a type that this organisation is allowed to access",
}
],
}


@mock_aws
@mock_repository
def test_document_reference_invalid_json(repository: DocumentPointerRepository):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,17 @@ def handler(
base_url = f"https://{config.ENVIRONMENT}.api.service.nhs.uk/"
self_link = f"{base_url}record-locator/consumer/FHIR/R4/DocumentReference?subject:identifier=https://fhir.nhs.uk/Id/nhs-number|{params.nhs_number}"

if not validate_type(params.type, metadata.pointer_types):
allowed_types = (
metadata.nrl_permissions_policy.types
if metadata.nrl_permissions_policy
else metadata.pointer_types
)

if not validate_type(params.type, allowed_types):
logger.log(
LogReference.CONSEARCH002,
type=params.type,
pointer_types=metadata.pointer_types,
pointer_types=allowed_types,
)
return SpineErrorResponse.INVALID_CODE_SYSTEM(
diagnostics="Invalid query parameter (The provided type does not match the allowed types for this organisation)",
Expand All @@ -80,7 +86,7 @@ def handler(
if custodian_id:
self_link += f"&custodian:identifier=https://fhir.nhs.uk/Id/ods-organization-code|{custodian_id}"

pointer_types = [params.type.root] if params.type else metadata.pointer_types
pointer_types = [params.type.root] if params.type else allowed_types
if params.type:
self_link += f"&type={params.type.root}"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
from nrlf.consumer.fhir.r4.model import CodeableConcept, Identifier
from nrlf.core.constants import (
CATEGORY_ATTRIBUTES,
CLIENT_RP_DETAILS,
TYPE_ATTRIBUTES,
Categories,
PointerTypes,
V2Headers,
)
from nrlf.core.dynamodb.repository import DocumentPointer, DocumentPointerRepository
from nrlf.tests.data import load_document_reference
Expand Down Expand Up @@ -63,6 +65,60 @@ def test_search_document_reference_happy_path(
}


@mock_aws
@mock_repository
@patch("nrlf.core.decorators.get_pointer_permissions_v2")
def test_search_document_reference_happy_path_v2(
get_pointer_permissions_mock, repository: DocumentPointerRepository
):
doc_ref = load_document_reference("Y05868-736253002-Valid")
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
repository.create(doc_pointer)

v2_headers = create_headers(
additional_headers={
V2Headers.NHSD_END_USER_ORGANISATION_ODS: "Y05868",
V2Headers.NHSD_NRL_APP_ID: "Y05868-TestApp-12345678",
}
)
v2_headers.pop(CLIENT_RP_DETAILS)

get_pointer_permissions_mock.return_value = {
"access_controls": [],
"types": ["http://snomed.info/sct|736253002"],
}

event = create_test_api_gateway_event(
headers=v2_headers,
query_string_parameters={
"subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191",
},
)

result = handler(event, create_mock_context())
body = result.pop("body")

assert result == {
"statusCode": "200",
"headers": default_response_headers(),
"isBase64Encoded": False,
}

parsed_body = json.loads(body)
assert parsed_body == {
"resourceType": "Bundle",
"type": "searchset",
"link": [
{
"relation": "self",
"url": "https://pytest.api.service.nhs.uk/record-locator/consumer/FHIR/R4/DocumentReference?subject:identifier=https://fhir.nhs.uk/Id/nhs-number|6700028191",
}
],
"total": 1,
"entry": [{"resource": doc_ref.model_dump(exclude_none=True)}],
}


@mock_aws
@mock_repository
def test_search_document_reference_accession_number_in_pointer(
Expand Down Expand Up @@ -680,6 +736,65 @@ def test_search_document_reference_invalid_type(
}


@mock_aws
@mock_repository
@patch("nrlf.core.decorators.get_pointer_permissions_v2")
def test_search_document_reference_invalid_type_v2(
get_pointer_permissions_mock, repository: DocumentPointerRepository
):
v2_headers = create_headers(
additional_headers={
V2Headers.NHSD_END_USER_ORGANISATION_ODS: "Y05868",
V2Headers.NHSD_NRL_APP_ID: "Y05868-TestApp-12345678",
}
)
v2_headers.pop(CLIENT_RP_DETAILS)

get_pointer_permissions_mock.return_value = {
"access_controls": [],
"types": ["http://snomed.info/sct|736253002"],
}

event = create_test_api_gateway_event(
headers=v2_headers,
query_string_parameters={
"subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191",
"type": "http://snomed.info/sct|861421000000109",
},
)

result = handler(event, create_mock_context())
body = result.pop("body")

assert result == {
"statusCode": "400",
"headers": default_response_headers(),
"isBase64Encoded": False,
}

parsed_body = json.loads(body)
assert parsed_body == {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "code-invalid",
"details": {
"coding": [
{
"code": "INVALID_CODE_SYSTEM",
"display": "Invalid code system",
"system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode",
}
]
},
"diagnostics": "Invalid query parameter (The provided type does not match the allowed types for this organisation)",
"expression": ["type"],
}
],
}


@mock_aws
@mock_repository
def test_search_document_reference_invalid_category(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,17 @@ def handler(
base_url = f"https://{config.ENVIRONMENT}.api.service.nhs.uk/"
self_link = f"{base_url}record-locator/consumer/FHIR/R4/DocumentReference?subject:identifier=https://fhir.nhs.uk/Id/nhs-number|{body.nhs_number}"

if not validate_type(body.type, metadata.pointer_types):
allowed_types = (
metadata.nrl_permissions_policy.types
if metadata.nrl_permissions_policy
else metadata.pointer_types
)

if not validate_type(body.type, allowed_types):
logger.log(
LogReference.CONPOSTSEARCH002,
type=body.type,
pointer_types=metadata.pointer_types,
pointer_types=allowed_types,
)
return SpineErrorResponse.INVALID_CODE_SYSTEM(
diagnostics="The provided type does not match the allowed types for this organisation",
Expand All @@ -80,7 +86,7 @@ def handler(
if custodian_id:
self_link += f"&custodian:identifier=https://fhir.nhs.uk/Id/ods-organization-code|{custodian_id}"

pointer_types = [body.type.root] if body.type else metadata.pointer_types
pointer_types = [body.type.root] if body.type else allowed_types
if body.type:
self_link += f"&type={body.type.root}"

Expand Down
Loading
Loading