Skip to content

Commit 3d175f0

Browse files
committed
Merge remote-tracking branch 'origin/VED-26-permissions-api' into VED-382-backend-configurable-disease-mapping
# Conflicts: # backend/tests/test_fhir_controller.py
2 parents 377e48c + dfb2314 commit 3d175f0

File tree

7 files changed

+94
-106
lines changed

7 files changed

+94
-106
lines changed

backend/src/fhir_controller.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
)
3636
from models.utils.generic_utils import check_keys_in_sources
3737
from models.utils.permissions import get_supplier_permissions
38-
from models.utils.permission_checker import VaccinePermissionChecker
38+
from models.utils.permission_checker import ApiOperationCode, validate_permissions, _expand_permissions
3939
from pds_service import PdsService
4040
from parameter_parser import process_params, process_search_params, create_query_string
4141
import urllib.parse
@@ -271,11 +271,8 @@ def update_immunization(self, aws_event):
271271
# Validate if the imms resource does not exist - end
272272

273273
# Check vaccine type permissions on the existing record - start
274-
try:
275-
checker = VaccinePermissionChecker(imms_vax_type_perms)
276-
checker.validate(existing_record["VaccineType"], "update")
277-
except UnauthorizedVaxOnRecordError as unauthorized:
278-
return self.create_response(403, unauthorized.to_operation_outcome())
274+
if not validate_permissions(imms_vax_type_perms, ApiOperationCode.UPDATE, [existing_record["VaccineType"]]):
275+
return self.create_response(403, UnauthorizedVaxOnRecordError().to_operation_outcome())
279276
# Check vaccine type permissions on the existing record - end
280277

281278
existing_resource_version = int(existing_record["Version"])
@@ -428,11 +425,15 @@ def search_immunizations(self, aws_event: APIGatewayProxyEventV1) -> dict:
428425
return self.create_response(403, unauthorized.to_operation_outcome())
429426
# Check vaxx type permissions on the existing record - start
430427
try:
431-
checker = VaccinePermissionChecker(imms_vax_type_perms)
432-
vax_type_perms = checker.expanded_permissions
433-
operation_code = VaccinePermissionChecker.mapped_operations.get("search")
434-
vax_type_perm = [ vaccine_type for vaccine_type in search_params.immunization_targets
435-
if f"{vaccine_type.lower()}.{operation_code}" in vax_type_perms ]
428+
expanded_permissions = _expand_permissions(imms_vax_type_perms)
429+
vax_type_perm = [
430+
vaccine_type
431+
for vaccine_type in search_params.immunization_targets
432+
if ApiOperationCode.SEARCH in expanded_permissions.get(vaccine_type.lower(), [])
433+
]
434+
# vax_type_perms = _expand_permissions(imms_vax_type_perms, ApiOperationCode.SEARCH)
435+
# vax_type_perm = [ vaccine_type for vaccine_type in search_params.immunization_targets
436+
# if f"{vaccine_type.lower()}.{ApiOperationCode.SEARCH}" in vax_type_perms ]
436437
if not vax_type_perm:
437438
raise UnauthorizedVaxError
438439
except UnauthorizedVaxError as unauthorized:

backend/src/fhir_repository.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import boto3
99
import botocore.exceptions
1010
from boto3.dynamodb.conditions import Attr, Key
11-
from models.utils.permission_checker import VaccinePermissionChecker
11+
from models.utils.permission_checker import ApiOperationCode, validate_permissions
1212
from botocore.config import Config
1313
from models.errors import (
1414
ResourceNotFoundError,
@@ -95,8 +95,8 @@ def get_immunization_by_identifier(
9595
item = response["Items"][0]
9696
resp = dict()
9797
vaccine_type = self._vaccine_type(item["PatientSK"])
98-
checker = VaccinePermissionChecker(imms_vax_type_perms)
99-
checker.validate(vaccine_type, "search")
98+
if not validate_permissions(imms_vax_type_perms,ApiOperationCode.SEARCH, [vaccine_type]):
99+
raise UnauthorizedVaxError()
100100
resource = json.loads(item["Resource"])
101101
resp["id"] = resource.get("id")
102102
resp["version"] = int(response["Items"][0]["Version"])
@@ -112,17 +112,17 @@ def get_immunization_by_id(self, imms_id: str, imms_vax_type_perms: str) -> Opti
112112
if "DeletedAt" in response["Item"]:
113113
if response["Item"]["DeletedAt"] == "reinstated":
114114
vaccine_type = self._vaccine_type(response["Item"]["PatientSK"])
115-
checker = VaccinePermissionChecker(imms_vax_type_perms)
116-
checker.validate(vaccine_type, "read")
115+
if not validate_permissions(imms_vax_type_perms,ApiOperationCode.READ, [vaccine_type]):
116+
raise UnauthorizedVaxError()
117117
resp["Resource"] = json.loads(response["Item"]["Resource"])
118118
resp["Version"] = response["Item"]["Version"]
119119
return resp
120120
else:
121121
return None
122122
else:
123123
vaccine_type = self._vaccine_type(response["Item"]["PatientSK"])
124-
checker = VaccinePermissionChecker(imms_vax_type_perms)
125-
checker.validate(vaccine_type, "read")
124+
if not validate_permissions(imms_vax_type_perms,ApiOperationCode.READ, [vaccine_type]):
125+
raise UnauthorizedVaxError()
126126
resp["Resource"] = json.loads(response["Item"]["Resource"])
127127
resp["Version"] = response["Item"]["Version"]
128128
return resp
@@ -168,8 +168,8 @@ def create_immunization(
168168
new_id = str(uuid.uuid4())
169169
immunization["id"] = new_id
170170
attr = RecordAttributes(immunization, patient)
171-
checker = VaccinePermissionChecker(imms_vax_type_perms)
172-
checker.validate(attr.vaccine_type, "create")
171+
if not validate_permissions(imms_vax_type_perms,ApiOperationCode.CREATE, [attr.vaccine_type]):
172+
raise UnauthorizedVaxError()
173173
query_response = _query_identifier(self.table, "IdentifierGSI", "IdentifierPK", attr.identifier)
174174

175175
if query_response is not None:
@@ -270,8 +270,7 @@ def update_reinstated_immunization(
270270
)
271271

272272
def _handle_permissions(self, imms_vax_type_perms: list[str], attr: RecordAttributes):
273-
checker = VaccinePermissionChecker(imms_vax_type_perms)
274-
checker.validate(attr.vaccine_type, "update")
273+
validate_permissions(imms_vax_type_perms, ApiOperationCode.UPDATE, [attr.vaccine_type])
275274

276275
def _build_update_expression(self, is_reinstate: bool) -> str:
277276
if is_reinstate:
@@ -367,8 +366,8 @@ def delete_immunization(
367366
if resp["Item"]["DeletedAt"] == "reinstated":
368367
pass
369368
vaccine_type = self._vaccine_type(resp["Item"]["PatientSK"])
370-
checker = VaccinePermissionChecker(imms_vax_type_perms)
371-
checker.validate(vaccine_type, "delete")
369+
if not validate_permissions(imms_vax_type_perms, ApiOperationCode.DELETE, [vaccine_type]):
370+
raise UnauthorizedVaxError()
372371

373372
response = self.table.update_item(
374373
Key={"PK": _make_immunization_pk(imms_id)},
Lines changed: 28 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,29 @@
1-
import json
2-
from clients import redis_client
3-
from models.errors import UnauthorizedVaxOnRecordError
4-
5-
6-
class VaccinePermissionChecker:
7-
"""Centralized vaccine permission checker."""
8-
9-
mapped_operations = {
10-
"create": "c",
11-
"read": "r",
12-
"update": "u",
13-
"delete": "d",
14-
"search": "s"
15-
}
16-
17-
def __init__(self, supplier_system: str):
18-
self.supplier_permissions = supplier_system
19-
self.expanded_permissions = self._expand_permissions(self.supplier_permissions)
20-
21-
# Expand permissions from the supplier's permission list
22-
@staticmethod
23-
def _expand_permissions(supplier_permissions: list[str]) -> set[str]:
24-
expanded = set()
25-
for permissions in supplier_permissions:
26-
if '.' not in permissions:
27-
continue # skip invalid format
28-
vaccine_type, allowed_operations = permissions.split('.', 1)
1+
from enum import StrEnum
2+
3+
class ApiOperationCode(StrEnum):
4+
CREATE = "c"
5+
READ = "r"
6+
UPDATE = "u"
7+
DELETE = "d"
8+
SEARCH = "s"
9+
10+
def _expand_permissions(permissions: list[str]) -> dict[str, list[ApiOperationCode]]:
11+
expanded_permissions = {}
12+
for permission in permissions:
13+
vaccine_type, operation_codes_str = permission.split(".", maxsplit=1)
2914
vaccine_type = vaccine_type.lower()
30-
for operation in allowed_operations.lower():
31-
if operation not in {'c', 'r', 'u', 'd', 's'}:
32-
raise ValueError(f"Unknown operation code: {operation} in a permission {permissions}")
33-
expanded.add(f"{vaccine_type}.{operation}")
34-
return expanded
35-
36-
# Check if the requested permission is a subset of the expanded permissions
37-
def _vaccine_permission(self, vaccine_type, operation) -> set:
38-
39-
operation = self.mapped_operations.get(operation.lower())
40-
if not operation:
41-
raise ValueError(f"Unsupported operation: {operation}")
42-
43-
vaccine_permission = set()
44-
if isinstance(vaccine_type, list):
45-
for x in vaccine_type:
46-
vaccine_permission.add(str.lower(f"{x}.{operation}"))
47-
return vaccine_permission
48-
else:
49-
vaccine_permission.add(str.lower(f"{vaccine_type}.{operation}"))
50-
return vaccine_permission
51-
52-
# Check if the requested permission is allowed
53-
def _check_permission(self, requested: set[str]) -> None:
54-
if not requested.issubset(self.expanded_permissions):
55-
raise UnauthorizedVaxOnRecordError()
56-
return None
57-
58-
# Validate the requested vaccine type and operation against the supplier permissions
59-
def validate(self, vaccine_type, operation) -> None:
60-
requested_perm = self._vaccine_permission(vaccine_type, operation)
61-
self._check_permission(requested_perm)
15+
operation_codes = [
16+
operation_code
17+
for operation_code in operation_codes_str.lower()
18+
if operation_code in list(ApiOperationCode)
19+
]
20+
expanded_permissions[vaccine_type] = operation_codes
21+
return expanded_permissions
22+
23+
def validate_permissions(permissions: list[str], operation: ApiOperationCode, vaccine_types: list[str]):
24+
expanded_permissions = _expand_permissions(permissions)
25+
print(f"operation: {operation}, expanded_permissions: {expanded_permissions}, vaccine_types: {vaccine_types}")
26+
return all([
27+
operation in expanded_permissions.get(vaccine_type.lower(), [])
28+
for vaccine_type in vaccine_types
29+
])

backend/tests/sample_data/permissions_config.py

Whitespace-only changes.

backend/tests/test_fhir_controller.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def test_get_imms_by_identifer_header_missing(self):
151151
response = self.controller.get_immunization_by_identifier(lambda_event)
152152

153153
self.assertEqual(response["statusCode"], 403)
154-
154+
155155
@patch("fhir_controller.get_supplier_permissions")
156156
def test_not_found_for_identifier(self, mock_get_permissions):
157157
"""it should return not-found OperationOutcome if it doesn't exist"""
@@ -183,7 +183,7 @@ def test_not_found_for_identifier(self, mock_get_permissions):
183183

184184
imms = identifier.replace("|", "#")
185185
# When
186-
186+
187187
response = self.controller.get_immunization_by_identifier(lambda_event)
188188

189189
# Then
@@ -218,7 +218,7 @@ def test_get_imms_by_identifer_patient_identifier_and_element_present(self, mock
218218
self.assertEqual(response["statusCode"], 400)
219219
body = json.loads(response["body"])
220220
self.assertEqual(body["resourceType"], "OperationOutcome")
221-
221+
222222
@patch("fhir_controller.get_supplier_permissions")
223223
def test_get_imms_by_identifer_both_body_and_query_params_present(self, mock_get_supplier_permissions):
224224
"""it should return Immunization Id if it exists"""
@@ -440,7 +440,7 @@ def test_validate_immunization_identifier_having_whitespace(self,mock_get_suppli
440440
self.assertEqual(response["statusCode"], 400)
441441
outcome = json.loads(response["body"])
442442
self.assertEqual(outcome["resourceType"], "OperationOutcome")
443-
443+
444444
@patch("fhir_controller.get_supplier_permissions")
445445
def test_validate_imms_id_invalid_vaccinetype(self, mock_get_permissions):
446446
"""it should validate lambda's Immunization id"""
@@ -756,7 +756,7 @@ def test_validate_immunization_identifier_having_whitespace(self,mock_get_suppli
756756
self.assertEqual(response["statusCode"], 400)
757757
outcome = json.loads(response["body"])
758758
self.assertEqual(outcome["resourceType"], "OperationOutcome")
759-
759+
760760
@patch("fhir_controller.get_supplier_permissions")
761761
def test_validate_imms_id_invalid_vaccinetype(self, mock_get_permissions):
762762
"""it should validate lambda's Immunization id"""
@@ -838,7 +838,7 @@ def test_get_imms_by_id_unauthorised_vax_error(self,mock_permissions):
838838
# Then
839839
mock_permissions.assert_called_once_with("test")
840840
self.assertEqual(response["statusCode"], 403)
841-
841+
842842
@patch("fhir_controller.get_supplier_permissions")
843843
def test_get_imms_by_id_no_vax_permission(self, mock_permissions):
844844
"""it should return Immunization Id if it exists"""
@@ -901,7 +901,7 @@ def setUp(self):
901901
@patch("fhir_controller.get_supplier_permissions")
902902
def test_create_immunization(self,mock_get_permissions):
903903
"""it should create Immunization and return resource's location"""
904-
mock_get_permissions.return_value = ["COVID19.CRUDS"]
904+
mock_get_permissions.return_value = ["COVID19.CRUDS", "FLU.CRUDS"]
905905
imms_id = str(uuid.uuid4())
906906
imms = create_covid_19_immunization(imms_id)
907907
aws_event = {
@@ -914,7 +914,7 @@ def test_create_immunization(self,mock_get_permissions):
914914

915915
imms_obj = json.loads(aws_event["body"])
916916
mock_get_permissions.assert_called_once_with("Test")
917-
self.service.create_immunization.assert_called_once_with(imms_obj, ["COVID19.CRUDS"], "Test")
917+
self.service.create_immunization.assert_called_once_with(imms_obj, ["COVID19.CRUDS", "FLU.CRUDS"], "Test")
918918
self.assertEqual(response["statusCode"], 201)
919919
self.assertTrue("body" not in response)
920920
self.assertTrue(response["headers"]["Location"].endswith(f"Immunization/{imms_id}"))
@@ -1110,7 +1110,7 @@ def test_update_immunization_etag_missing(self, mock_get_supplier_permissions):
11101110
@patch("fhir_controller.get_supplier_permissions")
11111111
def test_update_immunization_duplicate(self, mock_get_supplier_permissions):
11121112
"""it should not update the Immunization record"""
1113-
mock_get_supplier_permissions.return_value = ["Covid19.U"]
1113+
mock_get_supplier_permissions.return_value = ["COVID19.U"]
11141114
# Given
11151115
imms_id = "valid-id"
11161116
imms = {"id": "valid-id"}
@@ -1160,11 +1160,11 @@ def test_update_immunization_UnauthorizedVaxError(self, mock_get_supplier_permis
11601160
response = self.controller.update_immunization(aws_event)
11611161
mock_get_supplier_permissions.assert_called_once_with("Test")
11621162
self.assertEqual(response["statusCode"], 403)
1163-
1163+
11641164
@patch("fhir_controller.get_supplier_permissions")
11651165
def test_update_immunization_UnauthorizedVaxError_check_for_non_batch(self, mock_get_supplier_permissions):
11661166
"""it should not update the Immunization record"""
1167-
mock_get_supplier_permissions.return_value = ["COVID19.U"]
1167+
mock_get_supplier_permissions.return_value = ["COVID19.CRDS"]
11681168
imms_id = "valid-id"
11691169
imms = {"id": "valid-id"}
11701170
aws_event = {
@@ -1591,7 +1591,7 @@ def test_immunization_exception_not_found(self, mock_get_permissions):
15911591
body = json.loads(response["body"])
15921592
self.assertEqual(body["resourceType"], "OperationOutcome")
15931593
self.assertEqual(body["issue"][0]["code"], "not-found")
1594-
1594+
15951595
@patch("fhir_controller.get_supplier_permissions")
15961596
def test_immunization_unhandled_error(self, mock_get_supplier_permissions):
15971597
"""it should return server-error OperationOutcome if service throws UnhandledResponseError"""
@@ -1635,7 +1635,7 @@ def tearDown(self):
16351635
def test_get_search_immunizations(self, mock_get_supplier_permissions):
16361636
"""it should search based on patient_identifier and immunization_target"""
16371637

1638-
mock_get_supplier_permissions.return_value = ["covid19.s"]
1638+
mock_get_supplier_permissions.return_value = ["COVID19.S"]
16391639
search_result = Bundle.construct()
16401640
self.service.search_immunizations.return_value = search_result
16411641

@@ -1818,7 +1818,7 @@ def test_post_search_immunizations(self,mock_get_supplier_permissions):
18181818
"headers": {"Content-Type": "application/x-www-form-urlencoded", "SupplierSystem": "Test"},
18191819
"body": base64_encoded_body,
18201820
}
1821-
1821+
18221822
# When
18231823
response = self.controller.search_immunizations(lambda_event)
18241824
# Then
@@ -1999,7 +1999,7 @@ def test_search_immunizations_returns_200_remove_vaccine_not_done(self, mock_get
19991999
"This method should return 200 but remove the data which has status as not done."
20002000
search_result = load_json_data("sample_immunization_response _for _not_done_event.json")
20012001
bundle = Bundle.parse_obj(search_result)
2002-
mock_get_supplier_permissions.return_value = ["covid19.cruds"]
2002+
mock_get_supplier_permissions.return_value = ["COVID19.CRUDS"]
20032003
self.service.search_immunizations.return_value = bundle
20042004
vaccine_type = "COVID19"
20052005
lambda_event = {

0 commit comments

Comments
 (0)