Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<AssignMessage async="false" continueOnError="false" enabled="true" name="AssignMessage.OperationOutcomeErrorResponse">
<Set>
<StatusCode>{status_code}</StatusCode>
<ReasonPhrase>Unauthorized</ReasonPhrase>
<ReasonPhrase>{reason_phrase}</ReasonPhrase>
<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>
</Set>
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<AssignMessage enabled="true" name="AssignMessage.SetOperationOutcomeInvalidBusinessFunction">
<AssignVariable>
<Name>op_outcome_issue_code</Name>
<Value>forbidden</Value>
</AssignVariable>
<AssignVariable>
<Name>faultstring</Name>
<Value>User does not have the required Business Function at the specified Organisation.</Value>
</AssignVariable>
<AssignVariable>
<Name>status_code</Name>
<Value>403</Value>
</AssignVariable>
<AssignVariable>
<Name>reason_phrase</Name>
<Value>Forbidden</Value>
</AssignVariable>
</AssignMessage>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<Name>status_code</Name>
<Value>401</Value>
</AssignVariable>
<AssignVariable>
<Name>reason_phrase</Name>
<Value>Unauthorized</Value>
</AssignVariable>
<AssignVariable>
<Name>op_outcome_issue_code</Name>
<Value>login</Value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<Name>status_code</Name>
<Value>401</Value>
</AssignVariable>
<AssignVariable>
<Name>reason_phrase</Name>
<Value>Unauthorized</Value>
</AssignVariable>
<AssignVariable>
<Name>op_outcome_issue_code</Name>
<Value>forbidden</Value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@
<Name>status_code</Name>
<Value>403</Value>
</AssignVariable>
<AssignVariable>
<Name>reason_phrase</Name>
<Value>Forbidden</Value>
</AssignVariable>
</AssignMessage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<RaiseFault async="false" continueOnError="false" enabled="true" name="RaiseFault.InvalidBusinessFunction">
<FaultResponse>
<Set>
<Payload contentType="text/plain"/>
<StatusCode>403</StatusCode>
<ReasonPhrase>Forbidden</ReasonPhrase>
</Set>
</FaultResponse>
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</RaiseFault>
23 changes: 20 additions & 3 deletions proxies/live/apiproxy/targets/ers-target.xml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,23 @@
</Step>
<Condition>(raisefault.RaiseFault.MissingAsid.failed = true)</Condition>
</FaultRule>
<FaultRule name="invalid_business_function">
<Step>
<Condition>(isFhirR4Path = true)</Condition>
<Name>AssignMessage.SetOperationOutcomeVariablesR4</Name>
</Step>
<Step>
<Condition>(isFhirR4Path = false)</Condition>
<Name>AssignMessage.SetOperationOutcomeVariablesPreR4</Name>
</Step>
<Step>
<Name>AssignMessage.SetOperationOutcomeInvalidBusinessFunction</Name>
</Step>
<Step>
<Name>AssignMessage.OperationOutcomeErrorResponse</Name>
</Step>
<Condition>(raisefault.RaiseFault.InvalidBusinessFunction.failed = true)</Condition>
</FaultRule>
</FaultRules>
<PreFlow>
<Request>
Expand Down Expand Up @@ -154,9 +171,9 @@
<Flows>
<Flow name="user-restricted-flow">
<Condition>(accesstoken.auth_type == "user")</Condition>
<Request><!--AUTHORISED_APPLICATION business function is not supported in user restricted flow --><Step>
<Name>RaiseFault.403Forbidden</Name>
<Condition>(request.header.nhsd-ers-business-function == "AUTHORISED_APPLICATION")</Condition>
<Request><!--AUTHORISED_APPLICATION business functions are not supported in user restricted flow --><Step>
<Name>RaiseFault.InvalidBusinessFunction</Name>
<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>
</Step> <Step>
<Name>AssignMessage.Set.x-ers-access-mode-header-user-restricted</Name>
</Step> <Step>
Expand Down
37 changes: 36 additions & 1 deletion tests/integration/test_app_restricted.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 75 additions & 1 deletion tests/integration/test_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] == (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to look this up! A ternary

"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",
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
):
Expand Down