Skip to content

Commit 44ed7ff

Browse files
Squashed commit of the following:
commit a50ffff Author: kevinmason-nhs <[email protected]> Date: Thu Sep 11 09:39:13 2025 +0100 tests passing, added 400 error case commit 29ea4f7 Author: kevinmason-nhs <[email protected]> Date: Wed Sep 10 16:28:28 2025 +0100 convert dict to string commit f5f1dfa Author: kevinmason-nhs <[email protected]> Date: Wed Sep 10 14:34:09 2025 +0100 fixes list not being a string commit bc8d928 Author: kevinmason-nhs <[email protected]> Date: Wed Sep 10 12:53:56 2025 +0100 define user and pass org code commit f6bfc54 Author: kevinmason-nhs <[email protected]> Date: Wed Sep 10 12:23:49 2025 +0100 fixes broken test commit 2486970 Author: kevinmason-nhs <[email protected]> Date: Wed Sep 10 11:36:01 2025 +0100 change approach with apim_app_flow_vars injection commit baa0bf9 Author: patrick cando <[email protected]> Date: Wed Sep 10 11:24:12 2025 +0100 Comment out ODS IT invalid tests commit b12bf0a Author: kevinmason-nhs <[email protected]> Date: Wed Sep 10 10:30:58 2025 +0100 adds logging for apim_app_flow_vars commit be591ce Author: kevinmason-nhs <[email protected]> Date: Wed Sep 10 10:06:44 2025 +0100 use a valid actor for user restricted testing commit c33d628 Author: kevinmason-nhs <[email protected]> Date: Wed Sep 10 09:48:45 2025 +0100 fixes variable name commit 7b6861c Author: kevinmason-nhs <[email protected]> Date: Wed Sep 10 09:11:37 2025 +0100 adds logging commit a4eeb80 Author: kevinmason-nhs <[email protected]> Date: Tue Sep 9 23:21:53 2025 +0100 fixes proxy and adds success cases
1 parent 91b4239 commit 44ed7ff

File tree

4 files changed

+236
-19
lines changed

4 files changed

+236
-19
lines changed

proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeServiceError.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<AssignMessage enabled="true" name="AssignMessage.SetOperationOutcomeODSHeaderMissingR4">
1+
<AssignMessage enabled="true" name="AssignMessage.SetOperationOutcomeServiceError">
22
<AssignVariable>
33
<Name>status_code</Name>
44
<Value>500</Value>

proxies/live/apiproxy/targets/ers-target.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@
150150
</Step>
151151
<Condition>(raisefault.RaiseFault.CheckAllowlistFailed.failed = true) and (validation.statusCode = 400)</Condition>
152152
</FaultRule>
153-
<FaultRule name="single_asid_app_misconfigured">
153+
<FaultRule name="euo_allowlist_internal_error">
154154
<Step>
155155
<Condition>(isFhirR4Path = false)</Condition>
156156
<Name>AssignMessage.InternalServerError</Name>
@@ -181,7 +181,7 @@
181181
<Step>
182182
<Name>OauthV2.VerifyAccessToken</Name>
183183
</Step>
184-
<!-- Must be placed after Authentication -->
184+
<!-- https://nhsd-jira.digital.nhs.uk/browse/ERSSUP-86126 Single ASID Per Connecting Party. Only for user-restricted, excludes Practitioner Role endpoint which should be accessible without -->
185185
<Step>
186186
<Name>FlowCallout.ExtendedAttributes</Name>
187187
<Condition>(accesstoken.auth_type == "user") and (proxy.pathsuffix != "/FHIR/R4/PractitionerRole")</Condition>

tests/conftest.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33
import pytest_asyncio
44
import warnings
5+
import json
56

67
from uuid import uuid4
78
from typing import Collection, Callable, Generator, Dict
@@ -134,7 +135,7 @@ async def user_restricted_product(client, make_product):
134135
"urn:nhsd:apim:user-nhs-id:aal3:e-referrals-service-api",
135136
"urn:nhsd:apim:user-nhs-id:aal2:e-referrals-service-api",
136137
],
137-
additional_attributes=[{"name": "EUOAllowlist", "value": "false"}],
138+
additional_attributes=[{"name": "EUOAllowlistRequired", "value": "false"}],
138139
)
139140

140141
print(f"product created: {productName}")
@@ -288,9 +289,10 @@ async def user_restricted_app(
288289
):
289290
# Setup
290291
if apim_app_flow_vars is not None:
292+
odslist = json.dumps({"ers": {"allowListodsCode": apim_app_flow_vars}})
291293
app = await make_app(
292294
user_restricted_product,
293-
{"asid": asid, "apim-app-flow-vars": apim_app_flow_vars},
295+
{"asid": asid, "apim-app-flow-vars": odslist},
294296
)
295297
else:
296298
app = await make_app(user_restricted_product, {"asid": asid})
@@ -316,6 +318,7 @@ async def _make_app(product, custom_attributes={}):
316318
{"name": key, "value": value} for key, value in custom_attributes.items()
317319
]
318320
attributes.append({"name": "DisplayName", "value": app_name})
321+
print(f"App attributes: {attributes}")
319322

320323
body = {
321324
"apiProducts": [product],
@@ -336,7 +339,7 @@ async def _make_app(product, custom_attributes={}):
336339

337340
@pytest.fixture
338341
def authenticate_user(client, user_restricted_app, environment, oauth_url):
339-
async def _auth(actor: Actor, apim_app_flow_vars=None):
342+
async def _auth(actor: Actor):
340343
print(f"Attempting to authenticate: {actor}")
341344

342345
credentials = user_restricted_app["credentials"][0]

tests/integration/test_user_restricted.py

Lines changed: 227 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,45 +29,259 @@
2929

3030
@pytest.mark.integration_test
3131
class TestUserRestricted:
32+
3233
@pytest.mark.asyncio
3334
@pytest.mark.parametrize(
34-
"endpoint_url, is_fhir_4",
35-
[("", False), ("/FHIR/R4/", True), ("/FHIR/STU3/", False)],
35+
"endpoint_url, is_fhir_4, user, apim_app_flow_vars ",
36+
[
37+
("", False, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
38+
("/FHIR/R4/", True, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
39+
("/FHIR/STU3/", False, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
40+
],
3641
)
37-
async def test_user_restricted_invalid_ods_code(
42+
async def test_user_restricted_valid_ods_code(
3843
self,
3944
authenticate_user,
45+
service_url,
46+
user: Actor,
47+
asid,
4048
endpoint_url,
4149
is_fhir_4,
42-
service_url,
43-
update_user_restricted_product,
50+
apim_app_flow_vars,
4451
):
45-
access_code = await authenticate_user(
46-
referring_clinician_insufficient_ial, ["invalid_code"]
47-
)
52+
access_code = await authenticate_user(user)
4853

4954
client_request_headers = {
5055
_HEADER_ECHO: "", # enable echo target
5156
_HEADER_AUTHORIZATION: "Bearer " + access_code,
5257
_HEADER_REQUEST_ID: "DUMMY-VALUE",
5358
RenamedHeader.REFERRAL_ID.original: _EXPECTED_REFERRAL_ID,
5459
RenamedHeader.CORRELATION_ID.original: _EXPECTED_CORRELATION_ID,
55-
RenamedHeader.BUSINESS_FUNCTION.original: referring_clinician_insufficient_ial.business_function,
56-
RenamedHeader.ODS_CODE.original: referring_clinician_insufficient_ial.org_code,
60+
RenamedHeader.BUSINESS_FUNCTION.original: user.business_function,
61+
RenamedHeader.ODS_CODE.original: user.org_code,
5762
RenamedHeader.FILENAME.original: _EXPECTED_FILENAME,
5863
RenamedHeader.COMM_RULE_ORG.original: _EXPECTED_COMM_RULE_ORG,
5964
RenamedHeader.OBO_USER_ID.original: _EXPECTED_OBO_USER_ID,
6065
}
6166

6267
# Make the API call
63-
64-
# Make request with user with ODS code not in allow list (e.g. R69)
6568
response = requests.get(
6669
f"{service_url}{endpoint_url}", headers=client_request_headers
6770
)
6871

6972
# Verify the status
70-
# Verify 403 response with appropriate error message
73+
assert (
74+
response.status_code == 200
75+
), "Expected a 200 when accessing the api but got " + str(response.status_code)
76+
77+
@pytest.mark.asyncio
78+
@pytest.mark.parametrize(
79+
"endpoint_url, is_fhir_4, user ,apim_app_flow_vars",
80+
[
81+
("", False, Actor.RC_DEV, ["invalid_code"]),
82+
("/FHIR/R4/", True, Actor.RC_DEV, ["invalid_code"]),
83+
("/FHIR/STU3/", False, Actor.RC_DEV, ["invalid_code"]),
84+
],
85+
)
86+
async def test_user_restricted_invalid_ods_code(
87+
self,
88+
authenticate_user,
89+
service_url,
90+
user: Actor,
91+
asid,
92+
endpoint_url,
93+
is_fhir_4,
94+
apim_app_flow_vars,
95+
):
96+
access_code = await authenticate_user(user)
97+
98+
client_request_headers = {
99+
_HEADER_ECHO: "", # enable echo target
100+
_HEADER_AUTHORIZATION: "Bearer " + access_code,
101+
_HEADER_REQUEST_ID: "DUMMY-VALUE",
102+
RenamedHeader.REFERRAL_ID.original: _EXPECTED_REFERRAL_ID,
103+
RenamedHeader.CORRELATION_ID.original: _EXPECTED_CORRELATION_ID,
104+
RenamedHeader.BUSINESS_FUNCTION.original: user.business_function,
105+
RenamedHeader.ODS_CODE.original: user.org_code,
106+
RenamedHeader.FILENAME.original: _EXPECTED_FILENAME,
107+
RenamedHeader.COMM_RULE_ORG.original: _EXPECTED_COMM_RULE_ORG,
108+
RenamedHeader.OBO_USER_ID.original: _EXPECTED_OBO_USER_ID,
109+
}
110+
111+
# Make the API call
112+
response = requests.get(
113+
f"{service_url}{endpoint_url}", headers=client_request_headers
114+
)
115+
# Verify the status
71116
assert (
72117
response.status_code == 403
73118
), "Expected a 403 when accessing the api but got " + str(response.status_code)
119+
# Verify the OperationOutcome payload
120+
response_data = response.json()
121+
assert response_data["resourceType"] == "OperationOutcome"
122+
assert response_data["meta"]["lastUpdated"] is not None
123+
assert len(response_data["meta"]["profile"]) == 1
124+
assert response_data["meta"]["profile"][0] == (
125+
"https://www.hl7.org/fhir/R4/operationoutcome.html"
126+
if is_fhir_4
127+
else "https://fhir.nhs.uk/STU3/StructureDefinition/eRS-OperationOutcome-1"
128+
)
129+
assert len(response_data["issue"]) == 1
130+
issue = response_data["issue"][0]
131+
assert issue["severity"] == "error"
132+
assert issue["code"] == "security" if is_fhir_4 else "forbidden"
133+
assert issue["diagnostics"] == (
134+
"Unauthorised ODS code provided in NHSD-End-User-Organisation-ODS header"
135+
)
136+
assert len(issue["details"]["coding"]) == 1
137+
issue_details = issue["details"]["coding"][0]
138+
assert (
139+
issue_details["system"]
140+
== "https://fhir.nhs.uk/CodeSystem/NHSD-API-ErrorOrWarningCode"
141+
if is_fhir_4
142+
else "https://fhir.nhs.uk/STU3/CodeSystem/eRS-APIErrorCode-1"
143+
)
144+
assert (
145+
issue_details["code"] == "ACCESS_DENIED" if is_fhir_4 else "ACCESS_DENIED"
146+
)
147+
148+
@pytest.mark.asyncio
149+
@pytest.mark.parametrize(
150+
"endpoint_url, is_fhir_4, user ,apim_app_flow_vars",
151+
[
152+
("", False, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
153+
("/FHIR/R4/", True, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
154+
("/FHIR/STU3/", False, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
155+
],
156+
)
157+
async def test_user_restricted_missing_ods_header(
158+
self,
159+
authenticate_user,
160+
service_url,
161+
user: Actor,
162+
asid,
163+
endpoint_url,
164+
is_fhir_4,
165+
apim_app_flow_vars,
166+
):
167+
access_code = await authenticate_user(user)
168+
169+
client_request_headers = {
170+
_HEADER_ECHO: "", # enable echo target
171+
_HEADER_AUTHORIZATION: "Bearer " + access_code,
172+
_HEADER_REQUEST_ID: "DUMMY-VALUE",
173+
RenamedHeader.REFERRAL_ID.original: _EXPECTED_REFERRAL_ID,
174+
RenamedHeader.CORRELATION_ID.original: _EXPECTED_CORRELATION_ID,
175+
RenamedHeader.BUSINESS_FUNCTION.original: user.business_function,
176+
RenamedHeader.FILENAME.original: _EXPECTED_FILENAME,
177+
RenamedHeader.COMM_RULE_ORG.original: _EXPECTED_COMM_RULE_ORG,
178+
RenamedHeader.OBO_USER_ID.original: _EXPECTED_OBO_USER_ID,
179+
}
180+
181+
# Make the API call
182+
response = requests.get(
183+
f"{service_url}{endpoint_url}", headers=client_request_headers
184+
)
185+
# Verify the status
186+
assert (
187+
response.status_code == 400
188+
), "Expected a 400 when accessing the api but got " + str(response.status_code)
189+
# Verify the OperationOutcome payload
190+
response_data = response.json()
191+
assert response_data["resourceType"] == "OperationOutcome"
192+
assert response_data["meta"]["lastUpdated"] is not None
193+
assert len(response_data["meta"]["profile"]) == 1
194+
assert response_data["meta"]["profile"][0] == (
195+
"https://www.hl7.org/fhir/R4/operationoutcome.html"
196+
if is_fhir_4
197+
else "https://fhir.nhs.uk/STU3/StructureDefinition/eRS-OperationOutcome-1"
198+
)
199+
assert len(response_data["issue"]) == 1
200+
issue = response_data["issue"][0]
201+
assert issue["severity"] == "error"
202+
assert issue["code"] == "invalid" if is_fhir_4 else "invalid"
203+
assert issue["diagnostics"] == (
204+
"Missing or Empty NHSD-End-User-Organisation-ODS header."
205+
)
206+
assert len(issue["details"]["coding"]) == 1
207+
issue_details = issue["details"]["coding"][0]
208+
assert (
209+
issue_details["system"]
210+
== "https://fhir.nhs.uk/CodeSystem/NHSD-API-ErrorOrWarningCode"
211+
if is_fhir_4
212+
else "https://fhir.nhs.uk/STU3/CodeSystem/eRS-APIErrorCode-1"
213+
)
214+
assert (
215+
issue_details["code"] == "MISSING_HEADER" if is_fhir_4 else "MISSING_HEADER"
216+
)
217+
218+
@pytest.mark.asyncio
219+
@pytest.mark.parametrize(
220+
"endpoint_url, is_fhir_4, user ,apim_app_flow_vars",
221+
[
222+
("", False, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
223+
("/FHIR/R4/", True, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
224+
("/FHIR/STU3/", False, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
225+
],
226+
)
227+
async def test_user_restricted_missing_ods_code(
228+
self,
229+
authenticate_user,
230+
service_url,
231+
user: Actor,
232+
asid,
233+
endpoint_url,
234+
is_fhir_4,
235+
apim_app_flow_vars,
236+
):
237+
access_code = await authenticate_user(user)
238+
239+
client_request_headers = {
240+
_HEADER_ECHO: "", # enable echo target
241+
_HEADER_AUTHORIZATION: "Bearer " + access_code,
242+
_HEADER_REQUEST_ID: "DUMMY-VALUE",
243+
RenamedHeader.REFERRAL_ID.original: _EXPECTED_REFERRAL_ID,
244+
RenamedHeader.CORRELATION_ID.original: _EXPECTED_CORRELATION_ID,
245+
RenamedHeader.BUSINESS_FUNCTION.original: user.business_function,
246+
RenamedHeader.ODS_CODE.original: "",
247+
RenamedHeader.FILENAME.original: _EXPECTED_FILENAME,
248+
RenamedHeader.COMM_RULE_ORG.original: _EXPECTED_COMM_RULE_ORG,
249+
RenamedHeader.OBO_USER_ID.original: _EXPECTED_OBO_USER_ID,
250+
}
251+
252+
# Make the API call
253+
response = requests.get(
254+
f"{service_url}{endpoint_url}", headers=client_request_headers
255+
)
256+
# Verify the status
257+
assert (
258+
response.status_code == 400
259+
), "Expected a 400 when accessing the api but got " + str(response.status_code)
260+
# Verify the OperationOutcome payload
261+
response_data = response.json()
262+
assert response_data["resourceType"] == "OperationOutcome"
263+
assert response_data["meta"]["lastUpdated"] is not None
264+
assert len(response_data["meta"]["profile"]) == 1
265+
assert response_data["meta"]["profile"][0] == (
266+
"https://www.hl7.org/fhir/R4/operationoutcome.html"
267+
if is_fhir_4
268+
else "https://fhir.nhs.uk/STU3/StructureDefinition/eRS-OperationOutcome-1"
269+
)
270+
assert len(response_data["issue"]) == 1
271+
issue = response_data["issue"][0]
272+
assert issue["severity"] == "error"
273+
assert issue["code"] == "invalid" if is_fhir_4 else "invalid"
274+
assert issue["diagnostics"] == (
275+
"Missing or Empty NHSD-End-User-Organisation-ODS header."
276+
)
277+
assert len(issue["details"]["coding"]) == 1
278+
issue_details = issue["details"]["coding"][0]
279+
assert (
280+
issue_details["system"]
281+
== "https://fhir.nhs.uk/CodeSystem/NHSD-API-ErrorOrWarningCode"
282+
if is_fhir_4
283+
else "https://fhir.nhs.uk/STU3/CodeSystem/eRS-APIErrorCode-1"
284+
)
285+
assert (
286+
issue_details["code"] == "MISSING_HEADER" if is_fhir_4 else "MISSING_HEADER"
287+
)

0 commit comments

Comments
 (0)