diff --git a/proxies/live/apiproxy/policies/AssignMessage.OperationOutcomeErrorResponse.xml b/proxies/live/apiproxy/policies/AssignMessage.OperationOutcomeErrorResponse.xml index ca205b0fc..037554370 100644 --- a/proxies/live/apiproxy/policies/AssignMessage.OperationOutcomeErrorResponse.xml +++ b/proxies/live/apiproxy/policies/AssignMessage.OperationOutcomeErrorResponse.xml @@ -1,7 +1,7 @@ {status_code} - Unauthorized + {reason_phrase} { "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#" } ] } true diff --git a/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeInvalidBusinessFunction.xml b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeInvalidBusinessFunction.xml new file mode 100644 index 000000000..ac2bd2178 --- /dev/null +++ b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeInvalidBusinessFunction.xml @@ -0,0 +1,18 @@ + + + op_outcome_issue_code + forbidden + + + faultstring + User does not have the required Business Function at the specified Organisation. + + + status_code + 403 + + + reason_phrase + Forbidden + + diff --git a/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeIssueCodeLogin.xml b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeIssueCodeLogin.xml index 288ca58e5..e193b2f79 100644 --- a/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeIssueCodeLogin.xml +++ b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeIssueCodeLogin.xml @@ -3,6 +3,10 @@ status_code 401 + + reason_phrase + Unauthorized + op_outcome_issue_code login diff --git a/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeIssueIal.xml b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeIssueIal.xml index 4bed1bf31..2bbe9b7c6 100644 --- a/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeIssueIal.xml +++ b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeIssueIal.xml @@ -3,6 +3,10 @@ status_code 401 + + reason_phrase + Unauthorized + op_outcome_issue_code forbidden diff --git a/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeMissingAsid.xml b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeMissingAsid.xml index 8e1082bd8..869c7bbfb 100644 --- a/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeMissingAsid.xml +++ b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeMissingAsid.xml @@ -11,4 +11,8 @@ status_code 403 + + reason_phrase + Forbidden + diff --git a/proxies/live/apiproxy/policies/RaiseFault.InvalidBusinessFunction.xml b/proxies/live/apiproxy/policies/RaiseFault.InvalidBusinessFunction.xml new file mode 100644 index 000000000..27810cb06 --- /dev/null +++ b/proxies/live/apiproxy/policies/RaiseFault.InvalidBusinessFunction.xml @@ -0,0 +1,10 @@ + + + + + 403 + Forbidden + + + true + diff --git a/proxies/live/apiproxy/targets/ers-target.xml b/proxies/live/apiproxy/targets/ers-target.xml index 5ea639c61..4add6f9c2 100644 --- a/proxies/live/apiproxy/targets/ers-target.xml +++ b/proxies/live/apiproxy/targets/ers-target.xml @@ -106,6 +106,23 @@ (raisefault.RaiseFault.MissingAsid.failed = true) + + + (isFhirR4Path = true) + AssignMessage.SetOperationOutcomeVariablesR4 + + + (isFhirR4Path = false) + AssignMessage.SetOperationOutcomeVariablesPreR4 + + + AssignMessage.SetOperationOutcomeInvalidBusinessFunction + + + AssignMessage.OperationOutcomeErrorResponse + + (raisefault.RaiseFault.InvalidBusinessFunction.failed = true) + @@ -154,9 +171,9 @@ (accesstoken.auth_type == "user") - - RaiseFault.403Forbidden - (request.header.nhsd-ers-business-function == "AUTHORISED_APPLICATION") + + RaiseFault.InvalidBusinessFunction + (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") AssignMessage.Set.x-ers-access-mode-header-user-restricted diff --git a/tests/integration/test_app_restricted.py b/tests/integration/test_app_restricted.py index 9a323f78b..1d6916d59 100644 --- a/tests/integration/test_app_restricted.py +++ b/tests/integration/test_app_restricted.py @@ -46,7 +46,42 @@ async def test_authorised_application_not_supported_for_user_restricted( f"{service_url}{_SPECIALTY_REF_DATA_URL}", headers=client_request_headers ) - assert_error_response_with_body(response, _EXPECTED_CORRELATION_ID, 403) + assert response.status_code == 403, ( + "Expected 403 response when calling the endpoint. But instead received a " + + response.status_code + ) + + assert ( + response.headers[RenamedHeader.CORRELATION_ID.original] + == _EXPECTED_CORRELATION_ID + ) + + for renamed_header in RenamedHeader: + assert renamed_header.renamed not in response.headers + + # Verify the OperationOutcome payload + response_data = response.json() + assert response_data["resourceType"] == "OperationOutcome" + assert response_data["meta"]["lastUpdated"] is not None + assert len(response_data["meta"]["profile"]) == 1 + assert ( + response_data["meta"]["profile"][0] + == "https://fhir.nhs.uk/STU3/StructureDefinition/eRS-OperationOutcome-1" + ) + assert len(response_data["issue"]) == 1 + issue = response_data["issue"][0] + assert issue["severity"] == "error" + assert issue["code"] == "forbidden" + assert issue["diagnostics"] == ( + "User does not have the required Business Function at the specified Organisation." + ) + assert len(issue["details"]["coding"]) == 1 + issue_details = issue["details"]["coding"][0] + assert ( + issue_details["system"] + == "https://fhir.nhs.uk/STU3/CodeSystem/eRS-APIErrorCode-1" + ) + assert issue_details["code"] == "NO_ACCESS" def test_authorised_application_supported_for_app_restricted( self, app_restricted_access_code, service_url diff --git a/tests/integration/test_headers.py b/tests/integration/test_headers.py index 849a6d9b4..c379500ee 100644 --- a/tests/integration/test_headers.py +++ b/tests/integration/test_headers.py @@ -73,6 +73,78 @@ async def test_headers_on_echo_target( expected_amr, ) + @pytest.mark.asyncio + @pytest.mark.parametrize( + "business_function,endpoint_url,is_r4", + [ + ("PROVIDER_AUTHORISED_APPLICATION", "", False), + ("REFERRER_AUTHORISED_APPLICATION", "", False), + ("AUTHORISED_APPLICATION", "", False), + ("PROVIDER_AUTHORISED_APPLICATION", "/FHIR/STU3/", False), + ("REFERRER_AUTHORISED_APPLICATION", "/FHIR/STU3/", False), + ("AUTHORISED_APPLICATION", "/FHIR/STU3/", False), + ("PROVIDER_AUTHORISED_APPLICATION", "/FHIR/R4/", True), + ("REFERRER_AUTHORISED_APPLICATION", "/FHIR/R4/", True), + ("AUTHORISED_APPLICATION", "/FHIR/R4/", True), + ], + ) + async def test_headers_on_echo_target_with_app_restricted_business_function( + self, business_function, endpoint_url, is_r4, authenticate_user, service_url + ): + user = Actor.RC + access_code = await authenticate_user(user) + client_request_headers = { + _HEADER_ECHO: "", # enable echo target + _HEADER_AUTHORIZATION: "Bearer " + access_code, + _HEADER_REQUEST_ID: "DUMMY-VALUE", + RenamedHeader.REFERRAL_ID.original: _EXPECTED_REFERRAL_ID, + RenamedHeader.CORRELATION_ID.original: _EXPECTED_CORRELATION_ID, + RenamedHeader.BUSINESS_FUNCTION.original: business_function, + RenamedHeader.ODS_CODE.original: user.org_code, + RenamedHeader.FILENAME.original: _EXPECTED_FILENAME, + RenamedHeader.COMM_RULE_ORG.original: _EXPECTED_COMM_RULE_ORG, + RenamedHeader.OBO_USER_ID.original: _EXPECTED_OBO_USER_ID, + } + + # Make the API call + response = requests.get( + service_url + endpoint_url, headers=client_request_headers + ) + + assert response.status_code == 403, ( + "Expected a 403 response when attempting to call the endpoint, but instead received a " + + str(response.status_code) + ) + + assert response.reason == "Forbidden" + + # Verify the OperationOutcome payload + response_data = response.json() + assert response_data["resourceType"] == "OperationOutcome" + assert response_data["meta"]["lastUpdated"] is not None + assert len(response_data["meta"]["profile"]) == 1 + assert response_data["meta"]["profile"][0] == ( + "https://www.hl7.org/fhir/R4/operationoutcome.html" + if is_r4 + else "https://fhir.nhs.uk/STU3/StructureDefinition/eRS-OperationOutcome-1" + ) + assert len(response_data["issue"]) == 1 + issue = response_data["issue"][0] + assert issue["severity"] == "error" + assert issue["code"] == "forbidden" + assert issue["diagnostics"] == ( + "User does not have the required Business Function at the specified Organisation." + ) + assert len(issue["details"]["coding"]) == 1 + issue_details = issue["details"]["coding"][0] + assert ( + issue_details["system"] + == "https://fhir.nhs.uk/CodeSystem/NHSD-API-ErrorOrWarningCode" + if is_r4 + else "https://fhir.nhs.uk/STU3/CodeSystem/eRS-APIErrorCode-1" + ) + assert issue_details["code"] == "ACCESS_DENIED" if is_r4 else "NO_ACCESS" + @pytest.mark.asyncio @pytest.mark.parametrize( "endpoint_url,is_r4", @@ -438,6 +510,8 @@ def test_unknown_access_code( response.status_code ) + assert response.reason == "Unauthorized" + if not is_operation_outcome: assert len(response.content) == 0 else: @@ -475,7 +549,7 @@ def test_unknown_access_code( assert renamed_header.renamed not in client_response_headers @pytest.mark.asyncio - @pytest.mark.parametrize("service_name", [(None)]) + @pytest.mark.parametrize("service_name", [None]) async def test_access_code_not_supported( self, referring_clinician, authenticate_user, service_url ):