Skip to content

Commit d4ddbf5

Browse files
[ERSSUP-89806]-[MW]-[Updated app-restricted business function validation to use new business functions]-[JW]
1 parent 119f2c5 commit d4ddbf5

9 files changed

+172
-6
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<AssignMessage async="false" continueOnError="false" enabled="true" name="AssignMessage.OperationOutcomeErrorResponse">
22
<Set>
33
<StatusCode>{status_code}</StatusCode>
4-
<ReasonPhrase>Unauthorized</ReasonPhrase>
4+
<ReasonPhrase>{reason_phrase}</ReasonPhrase>
55
<Payload contentType="application/fhir+json" variablePrefix="%" variableSuffix="#">{ "resourceType": "OperationOutcome", "meta": { "lastUpdated": "%current_timestamp#", "profile" : [ "%op_outcome_fhir_profile#" ] }, "issue": [ { "severity": "error", "code": "%op_outcome_issue_code#", "details": { "coding": [ { "system": "%op_outcome_issue_details_coding_system#", "code": "%op_outcome_issue_details_coding_code#" } ] }, "diagnostics": "%faultstring#" } ] }</Payload>
66
</Set>
77
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<AssignMessage enabled="true" name="AssignMessage.SetOperationOutcomeInvalidBusinessFunction">
2+
<AssignVariable>
3+
<Name>op_outcome_issue_code</Name>
4+
<Value>forbidden</Value>
5+
</AssignVariable>
6+
<AssignVariable>
7+
<Name>faultstring</Name>
8+
<Value>User does not have the required Business Function at the specified Organisation.</Value>
9+
</AssignVariable>
10+
<AssignVariable>
11+
<Name>status_code</Name>
12+
<Value>403</Value>
13+
</AssignVariable>
14+
<AssignVariable>
15+
<Name>reason_phrase</Name>
16+
<Value>Forbidden</Value>
17+
</AssignVariable>
18+
</AssignMessage>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
<Name>status_code</Name>
44
<Value>401</Value>
55
</AssignVariable>
6+
<AssignVariable>
7+
<Name>reason_phrase</Name>
8+
<Value>Unauthorized</Value>
9+
</AssignVariable>
610
<AssignVariable>
711
<Name>op_outcome_issue_code</Name>
812
<Value>login</Value>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
<Name>status_code</Name>
44
<Value>401</Value>
55
</AssignVariable>
6+
<AssignVariable>
7+
<Name>reason_phrase</Name>
8+
<Value>Unauthorized</Value>
9+
</AssignVariable>
610
<AssignVariable>
711
<Name>op_outcome_issue_code</Name>
812
<Value>forbidden</Value>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@
1111
<Name>status_code</Name>
1212
<Value>403</Value>
1313
</AssignVariable>
14+
<AssignVariable>
15+
<Name>reason_phrase</Name>
16+
<Value>Forbidden</Value>
17+
</AssignVariable>
1418
</AssignMessage>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<RaiseFault async="false" continueOnError="false" enabled="true" name="RaiseFault.InvalidBusinessFunction">
2+
<FaultResponse>
3+
<Set>
4+
<Payload contentType="text/plain"/>
5+
<StatusCode>403</StatusCode>
6+
<ReasonPhrase>Forbidden</ReasonPhrase>
7+
</Set>
8+
</FaultResponse>
9+
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
10+
</RaiseFault>

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,23 @@
106106
</Step>
107107
<Condition>(raisefault.RaiseFault.MissingAsid.failed = true)</Condition>
108108
</FaultRule>
109+
<FaultRule name="invalid_business_function">
110+
<Step>
111+
<Condition>(isFhirR4Path = true)</Condition>
112+
<Name>AssignMessage.SetOperationOutcomeVariablesR4</Name>
113+
</Step>
114+
<Step>
115+
<Condition>(isFhirR4Path = false)</Condition>
116+
<Name>AssignMessage.SetOperationOutcomeVariablesPreR4</Name>
117+
</Step>
118+
<Step>
119+
<Name>AssignMessage.SetOperationOutcomeInvalidBusinessFunction</Name>
120+
</Step>
121+
<Step>
122+
<Name>AssignMessage.OperationOutcomeErrorResponse</Name>
123+
</Step>
124+
<Condition>(raisefault.RaiseFault.InvalidBusinessFunction.failed = true)</Condition>
125+
</FaultRule>
109126
</FaultRules>
110127
<PreFlow>
111128
<Request>
@@ -154,9 +171,9 @@
154171
<Flows>
155172
<Flow name="user-restricted-flow">
156173
<Condition>(accesstoken.auth_type == "user")</Condition>
157-
<Request><!--AUTHORISED_APPLICATION business function is not supported in user restricted flow --><Step>
158-
<Name>RaiseFault.403Forbidden</Name>
159-
<Condition>(request.header.nhsd-ers-business-function == "AUTHORISED_APPLICATION")</Condition>
174+
<Request><!--AUTHORISED_APPLICATION business functions are not supported in user restricted flow --><Step>
175+
<Name>RaiseFault.InvalidBusinessFunction</Name>
176+
<Condition>(request.header.nhsd-ers-business-function == "PROVIDER_AUTHORISED_APPLICATION") or (request.header.nhsd-ers-business-function == "REFERRER_AUTHORISED_APPLICATION") or (request.header.nhsd-ers-business-function == "AUTHORISED_APPLICATION")</Condition>
160177
</Step> <Step>
161178
<Name>AssignMessage.Set.x-ers-access-mode-header-user-restricted</Name>
162179
</Step> <Step>

tests/integration/test_app_restricted.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,42 @@ async def test_authorised_application_not_supported_for_user_restricted(
4646
f"{service_url}{_SPECIALTY_REF_DATA_URL}", headers=client_request_headers
4747
)
4848

49-
assert_error_response_with_body(response, _EXPECTED_CORRELATION_ID, 403)
49+
assert response.status_code == 403, (
50+
"Expected 403 response when calling the endpoint. But instead received a "
51+
+ response.status_code
52+
)
53+
54+
assert (
55+
response.headers[RenamedHeader.CORRELATION_ID.original]
56+
== _EXPECTED_CORRELATION_ID
57+
)
58+
59+
for renamed_header in RenamedHeader:
60+
assert renamed_header.renamed not in response.headers
61+
62+
# Verify the OperationOutcome payload
63+
response_data = response.json()
64+
assert response_data["resourceType"] == "OperationOutcome"
65+
assert response_data["meta"]["lastUpdated"] is not None
66+
assert len(response_data["meta"]["profile"]) == 1
67+
assert (
68+
response_data["meta"]["profile"][0]
69+
== "https://fhir.nhs.uk/STU3/StructureDefinition/eRS-OperationOutcome-1"
70+
)
71+
assert len(response_data["issue"]) == 1
72+
issue = response_data["issue"][0]
73+
assert issue["severity"] == "error"
74+
assert issue["code"] == "forbidden"
75+
assert issue["diagnostics"] == (
76+
"User does not have the required Business Function at the specified Organisation."
77+
)
78+
assert len(issue["details"]["coding"]) == 1
79+
issue_details = issue["details"]["coding"][0]
80+
assert (
81+
issue_details["system"]
82+
== "https://fhir.nhs.uk/STU3/CodeSystem/eRS-APIErrorCode-1"
83+
)
84+
assert issue_details["code"] == "NO_ACCESS"
5085

5186
def test_authorised_application_supported_for_app_restricted(
5287
self, app_restricted_access_code, service_url

tests/integration/test_headers.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,78 @@ async def test_headers_on_echo_target(
7373
expected_amr,
7474
)
7575

76+
@pytest.mark.asyncio
77+
@pytest.mark.parametrize(
78+
"business_function,endpoint_url,is_r4",
79+
[
80+
("PROVIDER_AUTHORISED_APPLICATION", "", False),
81+
("REFERRER_AUTHORISED_APPLICATION", "", False),
82+
("AUTHORISED_APPLICATION", "", False),
83+
("PROVIDER_AUTHORISED_APPLICATION", "/FHIR/STU3/", False),
84+
("REFERRER_AUTHORISED_APPLICATION", "/FHIR/STU3/", False),
85+
("AUTHORISED_APPLICATION", "/FHIR/STU3/", False),
86+
("PROVIDER_AUTHORISED_APPLICATION", "/FHIR/R4/", True),
87+
("REFERRER_AUTHORISED_APPLICATION", "/FHIR/R4/", True),
88+
("AUTHORISED_APPLICATION", "/FHIR/R4/", True),
89+
],
90+
)
91+
async def test_headers_on_echo_target_with_app_restricted_business_function(
92+
self, business_function, endpoint_url, is_r4, authenticate_user, service_url
93+
):
94+
user = Actor.RC
95+
access_code = await authenticate_user(user)
96+
client_request_headers = {
97+
_HEADER_ECHO: "", # enable echo target
98+
_HEADER_AUTHORIZATION: "Bearer " + access_code,
99+
_HEADER_REQUEST_ID: "DUMMY-VALUE",
100+
RenamedHeader.REFERRAL_ID.original: _EXPECTED_REFERRAL_ID,
101+
RenamedHeader.CORRELATION_ID.original: _EXPECTED_CORRELATION_ID,
102+
RenamedHeader.BUSINESS_FUNCTION.original: business_function,
103+
RenamedHeader.ODS_CODE.original: user.org_code,
104+
RenamedHeader.FILENAME.original: _EXPECTED_FILENAME,
105+
RenamedHeader.COMM_RULE_ORG.original: _EXPECTED_COMM_RULE_ORG,
106+
RenamedHeader.OBO_USER_ID.original: _EXPECTED_OBO_USER_ID,
107+
}
108+
109+
# Make the API call
110+
response = requests.get(
111+
service_url + endpoint_url, headers=client_request_headers
112+
)
113+
114+
assert response.status_code == 403, (
115+
"Expected a 403 response when attempting to call the endpoint, but instead received a "
116+
+ str(response.status_code)
117+
)
118+
119+
assert response.reason == "Forbidden"
120+
121+
# Verify the OperationOutcome payload
122+
response_data = response.json()
123+
assert response_data["resourceType"] == "OperationOutcome"
124+
assert response_data["meta"]["lastUpdated"] is not None
125+
assert len(response_data["meta"]["profile"]) == 1
126+
assert response_data["meta"]["profile"][0] == (
127+
"https://www.hl7.org/fhir/R4/operationoutcome.html"
128+
if is_r4
129+
else "https://fhir.nhs.uk/STU3/StructureDefinition/eRS-OperationOutcome-1"
130+
)
131+
assert len(response_data["issue"]) == 1
132+
issue = response_data["issue"][0]
133+
assert issue["severity"] == "error"
134+
assert issue["code"] == "forbidden"
135+
assert issue["diagnostics"] == (
136+
"User does not have the required Business Function at the specified Organisation."
137+
)
138+
assert len(issue["details"]["coding"]) == 1
139+
issue_details = issue["details"]["coding"][0]
140+
assert (
141+
issue_details["system"]
142+
== "https://fhir.nhs.uk/CodeSystem/NHSD-API-ErrorOrWarningCode"
143+
if is_r4
144+
else "https://fhir.nhs.uk/STU3/CodeSystem/eRS-APIErrorCode-1"
145+
)
146+
assert issue_details["code"] == "ACCESS_DENIED" if is_r4 else "NO_ACCESS"
147+
76148
@pytest.mark.asyncio
77149
@pytest.mark.parametrize(
78150
"endpoint_url,is_r4",
@@ -438,6 +510,8 @@ def test_unknown_access_code(
438510
response.status_code
439511
)
440512

513+
assert response.reason == "Unauthorized"
514+
441515
if not is_operation_outcome:
442516
assert len(response.content) == 0
443517
else:
@@ -475,7 +549,7 @@ def test_unknown_access_code(
475549
assert renamed_header.renamed not in client_response_headers
476550

477551
@pytest.mark.asyncio
478-
@pytest.mark.parametrize("service_name", [(None)])
552+
@pytest.mark.parametrize("service_name", [None])
479553
async def test_access_code_not_supported(
480554
self, referring_clinician, authenticate_user, service_url
481555
):

0 commit comments

Comments
 (0)