diff --git a/macros/manifest_macros.yml b/macros/manifest_macros.yml
index c9426290f..b2f063576 100644
--- a/macros/manifest_macros.yml
+++ b/macros/manifest_macros.yml
@@ -3,12 +3,14 @@
products:
{% endmacro %}
-{%- macro product(ENV, MODE, TITLE, product_name, display_name) -%}
+{%- macro product(ENV, MODE, TITLE, product_name, display_name, euo_allowlist_required) -%}
- name: e-referrals-service-api-{{ product_name }}{{ MODE.nameSuffix }}
approvalType: {{ ENV.approval_type | default('auto') }}
attributes:
- name: access
value: public
+ - name: EUOAllowlistRequired
+ value: {{ euo_allowlist_required }}
- name: ratelimiting
value:
e-referrals-service-api-{{ product_name }}:
diff --git a/manifest_template.yml b/manifest_template.yml
index 4c04cbe9c..24b473d8a 100644
--- a/manifest_template.yml
+++ b/manifest_template.yml
@@ -16,23 +16,31 @@ APIGEE_ENVIRONMENTS:
variants:
- name: rc-internal-dev
display_name: Internal Development - rc
+ euo_allowlist_required: false
- name: fix-internal-dev
display_name: Internal Development - fix
+ euo_allowlist_required: false
- name: fti-internal-dev
display_name: Internal Development - ft01
+ euo_allowlist_required: false
- name: ftiv-internal-dev
display_name: Internal Development - ft04
+ euo_allowlist_required: false
- name: ftv-internal-dev
display_name: Internal Development - ft05
+ euo_allowlist_required: false
- name: ftix-internal-dev
display_name: Internal Development - ft09
+ euo_allowlist_required: false
- name: ftxxii-internal-dev
display_name: Internal Development - ft22
+ euo_allowlist_required: false
- name: internal-dev-sandbox
variants:
- name: internal-dev-sandbox
display_name: Internal Development Sandbox
+ euo_allowlist_required: false
- name: int
additional_proxies:
@@ -41,6 +49,7 @@ APIGEE_ENVIRONMENTS:
variants:
- name: int
display_name: Integration Testing
+ euo_allowlist_required: false
- name: internal-qa
additional_proxies:
@@ -49,16 +58,19 @@ APIGEE_ENVIRONMENTS:
variants:
- name: internal-qa
display_name: Internal QA
+ euo_allowlist_required: false
- name: internal-qa-sandbox
variants:
- name: internal-qa-sandbox
display_name: Internal QA Sandbox
+ euo_allowlist_required: false
- name: sandbox
variants:
- name: sandbox
display_name: Sandbox
+ euo_allowlist_required: false
- name: dev
additional_proxies:
@@ -66,12 +78,14 @@ APIGEE_ENVIRONMENTS:
variants:
- name: dep-dev
display_name: Dev - dep
+ euo_allowlist_required: false
- name: prod
approval_type: manual
variants:
- name: prod
display_name: Production
+ euo_allowlist_required: false
ACCESS_MODES:
- name: healthcare-worker
@@ -104,7 +118,7 @@ apigee:
{% for VARIANT in ENV.variants %}
{% for MODE in ACCESS_MODES %}
- {{ macros.product(ENV, MODE, TITLE, VARIANT.name, VARIANT.display_name) }}
+ {{ macros.product(ENV, MODE, TITLE, VARIANT.name, VARIANT.display_name, VARIANT.euo_allowlist_required) }}
{% endfor %}
{% endfor %}
diff --git a/proxies/live/apiproxy/policies/AssignMessage.InternalServerError.xml b/proxies/live/apiproxy/policies/AssignMessage.InternalServerError.xml
new file mode 100644
index 000000000..321aacd0c
--- /dev/null
+++ b/proxies/live/apiproxy/policies/AssignMessage.InternalServerError.xml
@@ -0,0 +1,7 @@
+
+ AssignMessage.InternalServerError
+
+ Internal Server Error
+
+
+
diff --git a/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeODSHeaderMissingPreR4.xml b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeODSHeaderMissingPreR4.xml
new file mode 100644
index 000000000..a10372bb6
--- /dev/null
+++ b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeODSHeaderMissingPreR4.xml
@@ -0,0 +1,18 @@
+
+
+ status_code
+ 400
+
+
+ op_outcome_issue_code
+ required
+
+
+ faultstring
+ Missing or Empty NHSD-End-User-Organisation-ODS header.
+
+
+ op_outcome_issue_details_coding_code
+ MISSING_HEADER
+
+
diff --git a/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeODSHeaderMissingR4.xml b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeODSHeaderMissingR4.xml
new file mode 100644
index 000000000..0c3c731fe
--- /dev/null
+++ b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeODSHeaderMissingR4.xml
@@ -0,0 +1,19 @@
+
+
+ status_code
+ 400
+
+
+ op_outcome_issue_code
+ required
+
+
+
+ op_outcome_issue_details_coding_code
+ MISSING_HEADER
+
+
+ faultstring
+ Missing or Empty NHSD-End-User-Organisation-ODS header.
+
+
diff --git a/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeODSHeaderValueNotInPartnerListPreR4.xml b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeODSHeaderValueNotInPartnerListPreR4.xml
new file mode 100644
index 000000000..004f6489f
--- /dev/null
+++ b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeODSHeaderValueNotInPartnerListPreR4.xml
@@ -0,0 +1,18 @@
+
+
+ status_code
+ 403
+
+
+ op_outcome_issue_code
+ forbidden
+
+
+ faultstring
+ Unauthorised ODS code provided in NHSD-End-User-Organisation-ODS header
+
+
+ op_outcome_issue_details_coding_code
+ NO_ACCESS
+
+
diff --git a/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeODSHeaderValueNotInPartnerListR4.xml b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeODSHeaderValueNotInPartnerListR4.xml
new file mode 100644
index 000000000..9bea7a9a8
--- /dev/null
+++ b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeODSHeaderValueNotInPartnerListR4.xml
@@ -0,0 +1,18 @@
+
+
+ status_code
+ 403
+
+
+ op_outcome_issue_code
+ forbidden
+
+
+ faultstring
+ Unauthorised ODS code provided in NHSD-End-User-Organisation-ODS header
+
+
+ op_outcome_issue_details_coding_code
+ ACCESS_DENIED
+
+
diff --git a/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeServiceError.xml b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeServiceError.xml
new file mode 100644
index 000000000..aac05d6c0
--- /dev/null
+++ b/proxies/live/apiproxy/policies/AssignMessage.SetOperationOutcomeServiceError.xml
@@ -0,0 +1,19 @@
+
+
+ status_code
+ 500
+
+
+ op_outcome_issue_code
+ exception
+
+
+
+ op_outcome_issue_details_coding_code
+ SERVICE_ERROR
+
+
+ faultstring
+ Internal Server Error
+
+
diff --git a/proxies/live/apiproxy/policies/FlowCallout.EUOAllowlistVerify.xml b/proxies/live/apiproxy/policies/FlowCallout.EUOAllowlistVerify.xml
new file mode 100644
index 000000000..026d951c2
--- /dev/null
+++ b/proxies/live/apiproxy/policies/FlowCallout.EUOAllowlistVerify.xml
@@ -0,0 +1,5 @@
+
+
+ EUOAllowlistVerify
+ EUOAllowlistVerify
+
diff --git a/proxies/live/apiproxy/policies/FlowCallout.ExtendedAttributes.xml b/proxies/live/apiproxy/policies/FlowCallout.ExtendedAttributes.xml
new file mode 100644
index 000000000..c3a25f88d
--- /dev/null
+++ b/proxies/live/apiproxy/policies/FlowCallout.ExtendedAttributes.xml
@@ -0,0 +1,5 @@
+
+
+ Extract extended attribute
+ ExtendedAttributes
+
diff --git a/proxies/live/apiproxy/proxies/default.xml b/proxies/live/apiproxy/proxies/default.xml
index 4b9fb99ce..d4f94f692 100644
--- a/proxies/live/apiproxy/proxies/default.xml
+++ b/proxies/live/apiproxy/proxies/default.xml
@@ -1,40 +1,38 @@
-
-
-
-
-
- AssignMessage.AddPayloadToPing
-
-
- (proxy.pathsuffix MatchesPath "/_ping") and ((request.verb = "GET") or (request.verb = "HEAD"))
-
-
-
-
-
- KeyValueMapOperations.GetSharedSecureVariables
-
-
- (private.apigee.status-endpoint-api-key NotEquals request.header.apikey) or (private.apigee.status-endpoint-api-key Is null)
- RaiseFault.401Unauthorized
-
-
- ServiceCallout.CallHealthcheckEndpoint
-
-
-
-
- javascript.SetStatusResponse
-
-
- (proxy.pathsuffix MatchesPath "/_status") and ((request.verb = "GET") or (request.verb = "HEAD"))
-
+
+
+
+
+
+ AssignMessage.AddPayloadToPing
+
+
+ (proxy.pathsuffix MatchesPath "/_ping") and ((request.verb = "GET") or (request.verb = "HEAD"))
+
+
+
+
+
+ KeyValueMapOperations.GetSharedSecureVariables
+
+
+ (private.apigee.status-endpoint-api-key NotEquals request.header.apikey) or (private.apigee.status-endpoint-api-key Is null)
+ RaiseFault.401Unauthorized
+
+
+ ServiceCallout.CallHealthcheckEndpoint
+
+
+
+
+ javascript.SetStatusResponse
+
+
+ (proxy.pathsuffix MatchesPath "/_status") and ((request.verb = "GET") or (request.verb = "HEAD"))
+
-
-
@@ -42,16 +40,15 @@
-
{{ SERVICE_BASE_PATH }}
secure
- (proxy.pathsuffix MatchesPath "/_ping") and ((request.verb = "GET") or (request.verb = "HEAD"))
+ (proxy.pathsuffix MatchesPath "/_ping") and ((request.verb = "GET") or (request.verb = "HEAD"))
- (proxy.pathsuffix MatchesPath "/_status") and ((request.verb = "GET") or (request.verb = "HEAD"))
+ (proxy.pathsuffix MatchesPath "/_status") and ((request.verb = "GET") or (request.verb = "HEAD"))
e-referrals-service-api-target
diff --git a/proxies/live/apiproxy/targets/ers-target.xml b/proxies/live/apiproxy/targets/ers-target.xml
index 4add6f9c2..463040ddb 100644
--- a/proxies/live/apiproxy/targets/ers-target.xml
+++ b/proxies/live/apiproxy/targets/ers-target.xml
@@ -29,17 +29,17 @@
AssignMessage.SetOperationOutcomeVariablesPreR4
-
+
aalError == true
AssignMessage.SetOperationOutcomeIssueCodeLogin
-
+
aalError == true
AssignMessage.OAuthPolicyErrorResponse
-
+
aalError != true
@@ -123,6 +123,69 @@
(raisefault.RaiseFault.InvalidBusinessFunction.failed = true)
+
+
+ (isFhirR4Path = false)
+ AssignMessage.SetOperationOutcomeVariablesPreR4
+
+
+ (isFhirR4Path = false)
+ AssignMessage.SetOperationOutcomeODSHeaderValueNotInPartnerListPreR4
+
+
+ (isFhirR4Path = true)
+ AssignMessage.SetOperationOutcomeVariablesR4
+
+
+ (isFhirR4Path = true)
+ AssignMessage.SetOperationOutcomeODSHeaderValueNotInPartnerListR4
+
+
+ AssignMessage.OperationOutcomeErrorResponse
+
+ (raisefault.RaiseFault.CheckAllowlistFailed.failed = true) and (validation.statusCode = 403)
+
+
+
+ (isFhirR4Path = false)
+ AssignMessage.SetOperationOutcomeVariablesPreR4
+
+
+ (isFhirR4Path = false)
+ AssignMessage.SetOperationOutcomeODSHeaderMissingPreR4
+
+
+ (isFhirR4Path = true)
+ AssignMessage.SetOperationOutcomeVariablesR4
+
+
+ (isFhirR4Path = true)
+ AssignMessage.SetOperationOutcomeODSHeaderMissingR4
+
+
+ AssignMessage.OperationOutcomeErrorResponse
+
+ (raisefault.RaiseFault.CheckAllowlistFailed.failed = true) and (validation.statusCode = 400)
+
+
+
+ (isFhirR4Path = false)
+ AssignMessage.InternalServerError
+
+
+ (isFhirR4Path = true)
+ AssignMessage.SetOperationOutcomeVariablesR4
+
+
+ (isFhirR4Path = true)
+ AssignMessage.SetOperationOutcomeServiceError
+
+
+ (isFhirR4Path = true)
+ AssignMessage.OperationOutcomeErrorResponse
+
+ (raisefault.RaiseFault.CheckAllowlistFailed.failed = true) and (validation.statusCode = 500)
+
@@ -135,6 +198,15 @@
OauthV2.VerifyAccessToken
+
+
+ FlowCallout.ExtendedAttributes
+ (accesstoken.auth_type == "user") and (proxy.pathsuffix != "/FHIR/R4/PractitionerRole")
+
+
+ FlowCallout.EUOAllowlistVerify
+ (accesstoken.auth_type == "user") and (proxy.pathsuffix != "/FHIR/R4/PractitionerRole")
+
RaiseFault.MissingAsid
(app.asid == null) Or (app.asid == "")
@@ -153,8 +225,8 @@
-
+
AssignMessage.SetFlagCustomHeaderXRequestId
@@ -171,7 +243,7 @@
(accesstoken.auth_type == "user")
-
+
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")
@@ -198,35 +270,35 @@
(request.header.nhsd-ers-referral-id ~~ ".+")
AssignMessage.Remove.x-request-id-header
-
+
AssignMessage.Set.x-ers-authentication-assurance-level-header
-
+
AssignMessage.Set.x-ers-amr-header
-
+
AssignMessage.Set.x-ers-id-assurance-level-header
-
+
(request.header.x-ers-id-assurance-level LesserThan 3)
RaiseFault.401InsufficientIal
- {% if ALLOW_ECHO_TARGET | default(false) == true %}
+ {% if ALLOW_ECHO_TARGET | default(false) == true %}
AssignMessage.SetEchoTarget
(request.header.echo)
{% endif %} {% if '--ft-' in (ERS_TARGET_SERVER | default('e-referrals-service-api')) %}
AssignMessage.SetTruststore
-
+
(isEchoCall != true )
-
+
AssignMessage.SetEchoTruststore
(isEchoCall == true)
{% endif %}
-
+
AssignMessage.Swap.CorrelationHeader
(accesstoken.auth_type == "app")
-
+
RaiseFault.403Forbidden
(request.header.x-ers-ods-code)
@@ -243,26 +315,26 @@
AssignMessage.Set.nhsd-ers-business-function-header-app-restricted
AssignMessage.Set.x-ers-user-id-header-app-restricted
-
+
AssignMessage.Remove.x-request-id-header
- {% if ALLOW_ECHO_TARGET | default(false) == true %}
+ {% if ALLOW_ECHO_TARGET | default(false) == true %}
AssignMessage.SetEchoTarget
(request.header.echo)
{% endif %} {% if '--ft-' in (ERS_TARGET_SERVER | default('e-referrals-service-api')) %}
AssignMessage.SetTruststore
-
+
(isEchoCall != true )
-
+
AssignMessage.SetEchoTruststore
(isEchoCall == true)
{% endif %}
-
+
AssignMessage.Swap.CorrelationHeader
-
+
diff --git a/specification/components/r4/schemas/endpoints/a042-retrieve-attachment.yaml b/specification/components/r4/schemas/endpoints/a042-retrieve-attachment.yaml
index 41bc67c0d..96200ee7d 100644
--- a/specification/components/r4/schemas/endpoints/a042-retrieve-attachment.yaml
+++ b/specification/components/r4/schemas/endpoints/a042-retrieve-attachment.yaml
@@ -105,4 +105,4 @@ responses:
'500':
$ref: '../responses/InternalServerError.yaml'
'503':
- $ref: '../responses/ServiceUnavailable.yaml'
\ No newline at end of file
+ $ref: '../responses/ServiceUnavailable.yaml'
diff --git a/tests/conftest.py b/tests/conftest.py
index 97de92084..ceceb5d0a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2,6 +2,7 @@
import pytest
import pytest_asyncio
import warnings
+import json
from uuid import uuid4
from typing import Collection, Callable, Generator, Dict
@@ -79,6 +80,12 @@ def asid(is_mocked_environment):
)
+@pytest.fixture(scope="session")
+def apim_app_flow_vars(allowListodsCode=None):
+ if allowListodsCode is not None:
+ return {"ers": {"allowListodsCode": allowListodsCode}}
+
+
@pytest.fixture(scope="session")
def referring_clinician(is_mocked_environment):
return Actor.RC_DEV if is_mocked_environment else Actor.RC
@@ -127,7 +134,8 @@ async def user_restricted_product(client, make_product):
[
"urn:nhsd:apim:user-nhs-id:aal3:e-referrals-service-api",
"urn:nhsd:apim:user-nhs-id:aal2:e-referrals-service-api",
- ]
+ ],
+ additional_attributes=[{"name": "EUOAllowlistRequired", "value": "false"}],
)
print(f"product created: {productName}")
@@ -240,7 +248,7 @@ def _update_function(attr, value):
@pytest.fixture
def make_product(client, environment, service_name):
- async def _make_product(product_scopes):
+ async def _make_product(product_scopes, additional_attributes=None):
product = ApiProductsAPI(client=client)
proxies = [f"identity-service-mock-{environment}"]
@@ -253,6 +261,10 @@ async def _make_product(product_scopes):
{"name": "access", "value": "public"},
{"name": "ratelimit", "value": "10ps"},
]
+
+ if additional_attributes is not None:
+ attributes.extend(additional_attributes)
+
body = {
"proxies": proxies,
"scopes": product_scopes,
@@ -272,9 +284,18 @@ async def _make_product(product_scopes):
@pytest_asyncio.fixture
-async def user_restricted_app(client, make_app, user_restricted_product, asid):
+async def user_restricted_app(
+ client, make_app, user_restricted_product, asid, apim_app_flow_vars
+):
# Setup
- app = await make_app(user_restricted_product, {"asid": asid})
+ if apim_app_flow_vars is not None:
+ odslist = json.dumps({"ers": {"allowListodsCode": apim_app_flow_vars}})
+ app = await make_app(
+ user_restricted_product,
+ {"asid": asid, "apim-app-flow-vars": odslist},
+ )
+ else:
+ app = await make_app(user_restricted_product, {"asid": asid})
appName = app["name"]
print(f"App created: {appName}")
@@ -297,6 +318,7 @@ async def _make_app(product, custom_attributes={}):
{"name": key, "value": value} for key, value in custom_attributes.items()
]
attributes.append({"name": "DisplayName", "value": app_name})
+ print(f"App attributes: {attributes}")
body = {
"apiProducts": [product],
diff --git a/tests/integration/test_headers.py b/tests/integration/test_headers.py
index c379500ee..80b654208 100644
--- a/tests/integration/test_headers.py
+++ b/tests/integration/test_headers.py
@@ -32,7 +32,11 @@ class TestHeaders:
@pytest.mark.asyncio
@pytest.mark.parametrize("user", [Actor.RC, Actor.AAL2_USER])
async def test_headers_on_echo_target(
- self, authenticate_user, service_url, user: Actor, asid
+ self,
+ authenticate_user,
+ service_url,
+ user: Actor,
+ asid,
):
access_code = await authenticate_user(user)
diff --git a/tests/integration/test_user_restricted.py b/tests/integration/test_user_restricted.py
new file mode 100644
index 000000000..8d4af58ec
--- /dev/null
+++ b/tests/integration/test_user_restricted.py
@@ -0,0 +1,285 @@
+import pytest
+import requests
+from requests import Response
+from tests.data import RenamedHeader, Actor, UserAuthenticationLevel
+from tests.asserts import assert_ok_response
+
+_HEADER_AUTHORIZATION = "Authorization"
+_HEADER_ECHO = "echo" # enable echo target
+_HEADER_BASE_URL = "x-ers-network-baseurl"
+_HEADER_USER_ID = "x-ers-user-id"
+_HEADER_REQUEST_ID = "x-request-id"
+_HEADER_ASID = "xapi_asid"
+_HEADER_ACCESS_MODE = "x-ers-access-mode"
+_HEADER_AAL = "x-ers-authentication-assurance-level"
+_HEADER_AMR = "x-ers-amr"
+_HEADER_ID_ASSURANCE_LEVEL = "x-ers-id-assurance-level"
+
+_EXPECTED_REFERRAL_ID = "000000040032"
+_EXPECTED_CORRELATION_ID = "123123-123123-123123-123123"
+_EXPECTED_FILENAME = "mysuperfilename.txt"
+_EXPECTED_COMMA_FILENAME = "mysuper,filename.txt"
+_EXPECTED_COMM_RULE_ORG = "R100"
+_EXPECTED_OBO_USER_ID = "0123456789000"
+_EXPECTED_ACCESS_MODE = "user-restricted"
+
+_SPECIALTY_REF_DATA_URL = "/FHIR/STU3/CodeSystem/SPECIALTY"
+_SEARCH_HEALTHCARE_SERVICE_R4_URL = "/FHIR/R4/HealthcareService"
+
+
+@pytest.mark.integration_test
+class TestUserRestricted:
+
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize(
+ "endpoint_url, is_fhir_4, user, apim_app_flow_vars ",
+ [
+ ("", False, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
+ ("/FHIR/R4/", True, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
+ ("/FHIR/STU3/", False, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
+ ],
+ )
+ async def test_user_restricted_valid_ods_code(
+ self,
+ authenticate_user,
+ service_url,
+ user: Actor,
+ asid,
+ endpoint_url,
+ is_fhir_4,
+ apim_app_flow_vars,
+ ):
+ 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: user.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(
+ f"{service_url}{endpoint_url}", headers=client_request_headers
+ )
+
+ # Verify the status
+ assert (
+ response.status_code == 200
+ ), "Expected a 200 when accessing the api but got " + str(response.status_code)
+
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize(
+ "endpoint_url, is_fhir_4, user ,apim_app_flow_vars",
+ [
+ ("", False, Actor.RC_DEV, ["invalid_code"]),
+ ("/FHIR/R4/", True, Actor.RC_DEV, ["invalid_code"]),
+ ("/FHIR/STU3/", False, Actor.RC_DEV, ["invalid_code"]),
+ ],
+ )
+ async def test_user_restricted_invalid_ods_code(
+ self,
+ authenticate_user,
+ service_url,
+ user: Actor,
+ asid,
+ endpoint_url,
+ is_fhir_4,
+ apim_app_flow_vars,
+ ):
+ 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: user.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(
+ f"{service_url}{endpoint_url}", headers=client_request_headers
+ )
+ # Verify the status
+ assert (
+ response.status_code == 403
+ ), "Expected a 403 when accessing the api but got " + str(response.status_code)
+ # 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_fhir_4
+ 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" if is_fhir_4 else "forbidden"
+ assert issue["diagnostics"] == (
+ "Unauthorised ODS code provided in NHSD-End-User-Organisation-ODS header"
+ )
+ 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_fhir_4
+ else "https://fhir.nhs.uk/STU3/CodeSystem/eRS-APIErrorCode-1"
+ )
+ assert issue_details["code"] == "ACCESS_DENIED" if is_fhir_4 else "NO_ACCESS"
+
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize(
+ "endpoint_url, is_fhir_4, user ,apim_app_flow_vars",
+ [
+ ("", False, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
+ ("/FHIR/R4/", True, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
+ ("/FHIR/STU3/", False, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
+ ],
+ )
+ async def test_user_restricted_missing_ods_header(
+ self,
+ authenticate_user,
+ service_url,
+ user: Actor,
+ asid,
+ endpoint_url,
+ is_fhir_4,
+ apim_app_flow_vars,
+ ):
+ 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: user.business_function,
+ 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(
+ f"{service_url}{endpoint_url}", headers=client_request_headers
+ )
+ # Verify the status
+ assert (
+ response.status_code == 400
+ ), "Expected a 400 when accessing the api but got " + str(response.status_code)
+ # 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_fhir_4
+ 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"] == "required" if is_fhir_4 else "required"
+ assert issue["diagnostics"] == (
+ "Missing or Empty NHSD-End-User-Organisation-ODS header."
+ )
+ 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_fhir_4
+ else "https://fhir.nhs.uk/STU3/CodeSystem/eRS-APIErrorCode-1"
+ )
+ assert (
+ issue_details["code"] == "MISSING_HEADER" if is_fhir_4 else "MISSING_HEADER"
+ )
+
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize(
+ "endpoint_url, is_fhir_4, user ,apim_app_flow_vars",
+ [
+ ("", False, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
+ ("/FHIR/R4/", True, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
+ ("/FHIR/STU3/", False, Actor.RC_DEV, [Actor.RC_DEV.org_code]),
+ ],
+ )
+ async def test_user_restricted_missing_ods_code(
+ self,
+ authenticate_user,
+ service_url,
+ user: Actor,
+ asid,
+ endpoint_url,
+ is_fhir_4,
+ apim_app_flow_vars,
+ ):
+ 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: user.business_function,
+ RenamedHeader.ODS_CODE.original: "",
+ 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(
+ f"{service_url}{endpoint_url}", headers=client_request_headers
+ )
+ # Verify the status
+ assert (
+ response.status_code == 400
+ ), "Expected a 400 when accessing the api but got " + str(response.status_code)
+ # 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_fhir_4
+ 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"] == "required" if is_fhir_4 else "required"
+ assert issue["diagnostics"] == (
+ "Missing or Empty NHSD-End-User-Organisation-ODS header."
+ )
+ 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_fhir_4
+ else "https://fhir.nhs.uk/STU3/CodeSystem/eRS-APIErrorCode-1"
+ )
+ assert (
+ issue_details["code"] == "MISSING_HEADER" if is_fhir_4 else "MISSING_HEADER"
+ )