Skip to content
11 changes: 8 additions & 3 deletions layer/nrlf/core/authoriser.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ def get_pointer_permissions_v2(
ods_code = connection_metadata.ods_code
app_id = connection_metadata.nrl_app_id

key = f"{producer_or_consumer}/{app_id}/{ods_code}.json"
logger.log(LogReference.V2PERMISSIONS011, key=key)

# check for app-wide permissions
app_wide_key = f"{producer_or_consumer}/{app_id}.json"
if path.isfile(f"/opt/python/nrlf_permissions/{app_wide_key}"):
logger.log(LogReference.V2PERMISSIONS011, key=app_wide_key)
key = app_wide_key
else: # use org level
key = f"{producer_or_consumer}/{app_id}/{ods_code}.json"
logger.log(LogReference.V2PERMISSIONS011, key=key)
file_path = f"/opt/python/nrlf_permissions/{key}"

pointer_permissions = {}
Expand Down
31 changes: 30 additions & 1 deletion layer/nrlf/core/tests/test_authoriser.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_authoriser_parse_permission_file_with_permission_file():
new_callable=mock_open,
read_data='{"types": ["http://snomed.info/sct|736253001"]}',
)
def test_authoriser_get_v2_permissions_with_pointer_types(mock_file, mocker):
def test_authoriser_get_v2_permissions_with_org_pointer_types(mock_file, mocker):
spy = mocker.spy(logger, "log")

expected_lookup_key = "producer/ODS123-app-id/ODS123.json"
Expand All @@ -47,6 +47,35 @@ def test_authoriser_get_v2_permissions_with_pointer_types(mock_file, mocker):
spy.assert_called_with(LogReference.V2PERMISSIONS011, key=expected_lookup_key)


@patch(
"builtins.open",
new_callable=mock_open,
read_data='{"types": ["http://snomed.info/sct|736253001"]}',
)
@patch("os.path.isfile")
def test_authoriser_get_v2_permissions_with_app_pointer_types(
mock_isfile, mock_file, mocker
):
spy = mocker.spy(logger, "log")
mock_isfile.return_value = True

expected_lookup_key = "producer/ODS123-app-id.json"
connection_metadata = parse_headers(
create_headers(ods_code="ODS123", nrl_app_id="ODS123-app-id")
)
result = get_pointer_permissions_v2(
connection_metadata=connection_metadata,
request_path="/producer/DocumentReference/_search",
)

mock_file.assert_called_once_with(
f"/opt/python/nrlf_permissions/{expected_lookup_key}"
)
assert result.get("types") == ["http://snomed.info/sct|736253001"]

spy.assert_called_with(LogReference.V2PERMISSIONS011, key=expected_lookup_key)


def test_authoriser_parse_v2_permission_file_with_no_permission_file(mocker):
spy = mocker.spy(logger, "log")
expected_lookup_key = "consumer/NotAnApp/NotFound.json"
Expand Down
54 changes: 52 additions & 2 deletions scripts/get_s3_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,20 @@ def add_feature_test_files(local_path):
"""

print("Adding feature test v2 permissions to temporary directory...")
permissions = {
org_permissions = {
"consumer": [
(
"z00z-y11y-x22x",
"RX898",
[PointerTypes.MENTAL_HEALTH_PLAN.value],
[],
), # http://snomed.info/sct|736253002
(
"app-t004",
"ODS1",
[PointerTypes.PERSONALISED_CARE_AND_SUPPORT_PLAN.value],
[],
),
(
"z00z-y11y-x22x",
"4LLTYP35C",
Expand All @@ -84,6 +90,12 @@ def add_feature_test_files(local_path):
[PointerTypes.EOL_CARE_PLAN.value],
[],
), # http://snomed.info/sct|736373009
(
"app-t004",
"ODS1",
[PointerTypes.PERSONALISED_CARE_AND_SUPPORT_PLAN.value],
[],
),
(
"z00z-y11y-x22x",
"4LLTYP35P",
Expand All @@ -99,9 +111,47 @@ def add_feature_test_files(local_path):
pointer_types,
access_controls,
)
for actor_type, entries in permissions.items()
for actor_type, entries in org_permissions.items()
for app_id, ods_code, pointer_types, access_controls in entries
]
app_permissions = {
"consumer": [
("app-t001", [PointerTypes.MENTAL_HEALTH_PLAN.value], []),
(
"app-t002",
[
PointerTypes.ADVANCE_CARE_PLAN.value,
PointerTypes.EMERGENCY_HEALTHCARE_PLAN.value,
PointerTypes.NEWS2_CHART.value,
],
[],
),
("app-t004", [PointerTypes.APPOINTMENT.value], []),
],
"producer": [
("app-t001", [PointerTypes.EOL_COORDINATION_SUMMARY.value], []),
(
"app-t003",
[
PointerTypes.ADVANCE_CARE_PLAN.value,
PointerTypes.EMERGENCY_HEALTHCARE_PLAN.value,
PointerTypes.NEWS2_CHART.value,
],
[],
),
("app-t004", [PointerTypes.APPOINTMENT.value], []),
],
}
[
_write_permission_file(
Path.joinpath(local_path, actor_type),
app_id,
pointer_types,
access_controls,
)
for actor_type, entries in app_permissions.items()
for app_id, pointer_types, access_controls in entries
]


def download_files(s3_client, bucket_name, local_path, file_names, folders):
Expand Down
151 changes: 151 additions & 0 deletions tests/features/producer/v2-permissions-app-level.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
Feature: Producer v2 APP-LEVEL permissions by pointer type - Success and Failure Scenarios
For the v2 permissions model, permissions are resolved from a JSON file stored in the
nrlf_permissions Lambda layer. Permissions for the feature tests are baked into the layer by
`scripts/get_s3_permissions.py` at build time, so no dynamic seeding step is required for
success scenarios.

Scenario: HAPPY PATH V2 Permissions with access for pointer type - createDocumentReference
Given the application 'ProducerTest001' (ID 'app-t001') is registered to access the API
When producer v2 'ORGA' creates a DocumentReference with values:
| property | value |
| subject | 9278693472 |
| status | current |
| type | 861421000000109 |
| category | 734163000 |
| custodian | ORGA |
| author | HAR1 |
| url | https://example.org/my-doc.pdf |
| practiceSetting | 788002001 |
Then the response status code is 201
And the response is an OperationOutcome with 1 issue
And the OperationOutcome contains the issue:
"""
{
"severity": "information",
"code": "informational",
"details": {
"coding": [
{
"system": "https://fhir.nhs.uk/ValueSet/NRL-ResponseCode",
"code": "RESOURCE_CREATED",
"display": "Resource created"
}
]
},
"diagnostics": "The document has been created"
}
"""
And the response has a Location header
And the Location header starts with '/DocumentReference/ORGA-'
And the resource in the Location header exists with values:
| property | value |
| subject | 9278693472 |
| status | current |
| type | 861421000000109 |
| category | 734163000 |
| custodian | ORGA |
| author | HAR1 |
| url | https://example.org/my-doc.pdf |
| practiceSetting | 788002001 |

Scenario: V2 Permissions with no producer access at all (but app level consumer access for specified type)
Given the application 'ProducerTest002' (ID 'app-t002') is registered to access the API
When producer v2 'ORGA' creates a DocumentReference with values:
| property | value |
| subject | 9278693472 |
| status | current |
| type | 736366004 |
| category | 734163000 |
| custodian | ORGA |
| author | HAR1 |
| url | https://example.org/my-doc.pdf |
| practiceSetting | 788002001 |
Then the response status code is 403
And the response is an OperationOutcome with 1 issue
And the OperationOutcome contains the issue:
"""
{
"severity": "error",
"code": "forbidden",
"details": {
"coding": [
{
"system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode",
"code": "ACCESS DENIED",
"display": "Access has been denied to process this request"
}
]
},
"diagnostics": "Your organisation 'ORGA' does not have permission to access this resource. Contact the onboarding team."
}
"""

Scenario: V2 Permissions with no access to specified type
Given the application 'ProducerTest003' (ID 'app-t003') is registered to access the API
When producer v2 'ORGA' creates a DocumentReference with values:
| property | value |
| subject | 9278693472 |
| status | current |
| type | 749001000000101 |
| category | 419891008 |
| custodian | ORGA |
| author | HAR1 |
| url | https://example.org/my-doc.pdf |
| practiceSetting | 788002001 |
Then the response status code is 403
And the response is an OperationOutcome with 1 issue
And the OperationOutcome contains the issue:
"""
{
"severity": "error",
"code": "forbidden",
"details": {
"coding": [
{
"system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode",
"code": "AUTHOR_CREDENTIALS_ERROR",
"display": "Author credentials error"
}
]
},
"diagnostics": "The type of the provided DocumentReference is not in the list of allowed types for this organisation",
"expression": [
"type.coding[0].code"
]
}
"""

Scenario: V2 Permissions with org-level permissions for requested type but app level permissions for other types
Given the application 'ProducerTest004' (ID 'app-t004') is registered to access the API
When producer v2 'ODS1' creates a DocumentReference with values:
| property | value |
| subject | 9278693472 |
| status | current |
| type | 2181441000000107 |
| category | 734163000 |
| custodian | ODS1 |
| author | HAR1 |
| url | https://example.org/my-doc.pdf |
| practiceSetting | 788002001 |
Then the response status code is 403
And the response is an OperationOutcome with 1 issue
And the OperationOutcome contains the issue:
"""
{
"severity": "error",
"code": "forbidden",
"details": {
"coding": [
{
"system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode",
"code": "AUTHOR_CREDENTIALS_ERROR",
"display": "Author credentials error"
}
]
},
"diagnostics": "The type of the provided DocumentReference is not in the list of allowed types for this organisation",
"expression": [
"type.coding[0].code"
]
}
"""
Loading