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" + )