Skip to content

Commit 59c0b25

Browse files
committed
refactor some logic from review sections
1 parent c6f4b85 commit 59c0b25

22 files changed

+114
-341
lines changed

backend/src/authorization.py

Lines changed: 8 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from models.errors import UnauthorizedError
77

8-
PERMISSIONS_HEADER = "Permissions"
8+
99
AUTHENTICATION_HEADER = "AuthenticationType"
1010

1111

@@ -14,40 +14,16 @@ class UnknownPermission(RuntimeError):
1414
"""Error when the parsed value can't be converted to Permissions enum."""
1515

1616

17-
class EndpointOperation(Enum):
18-
"""The kind of operation.
19-
This maps one-to-one to each endpoint. Authorization class decides whether there are sufficient permissions or not.
20-
The caller is responsible for passing the correct operation.
21-
"""
22-
READ = 0,
23-
CREATE = 1,
24-
UPDATE = 2,
25-
DELETE = 3,
26-
SEARCH = 4,
27-
28-
2917
class AuthType(str, Enum):
3018
"""This backend supports all three types of authentication.
3119
An Apigee App should specify AuthenticationType in its custom attribute.
3220
Each Apigee app can only have one type of authentication which is enforced by onboarding process.
3321
See: https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation"""
3422
APP_RESTRICTED = "ApplicationRestricted",
35-
NHS_LOGIN = "NnsLogin",
23+
NHS_LOGIN = "NhsLogin",
3624
CIS2 = "Cis2",
3725

3826

39-
class Permission(str, Enum):
40-
"""Permission name for each operation that can be done to an Immunization Resource
41-
An Apigee App should specify a set of these as a comma-separated custom attribute.
42-
Permission works the same way as 'scope' but, in this case, they're called permission to distinguish them from
43-
OAuth2 scopes"""
44-
READ = "immunization:read"
45-
CREATE = "immunization:create"
46-
UPDATE = "immunization:update"
47-
DELETE = "immunization:delete"
48-
SEARCH = "immunization:search"
49-
50-
5127
class Authorization:
5228
""" Authorize the call based on the endpoint and the authentication type.
5329
This class uses the passed headers from Apigee to decide the type of authentication (Application Restricted,
@@ -56,52 +32,13 @@ class Authorization:
5632
UnknownPermission is due to proxy bad configuration, and should result in 500. Any invalid value, either
5733
insufficient permissions or bad string, will result in UnauthorizedError if it comes from user.
5834
"""
59-
60-
def authorize(self, operation: EndpointOperation, aws_event: dict):
35+
36+
def authorize(self, aws_event: dict):
6137
auth_type = self._parse_auth_type(aws_event["headers"])
62-
if auth_type == AuthType.APP_RESTRICTED:
63-
self._app_restricted(operation, aws_event)
64-
if auth_type == AuthType.CIS2:
65-
self._cis2(operation, aws_event)
66-
# TODO(NhsLogin_AMB-1923) add NHSLogin
67-
else:
68-
UnauthorizedError()
69-
70-
_app_restricted_map = {
71-
EndpointOperation.READ: {Permission.READ},
72-
EndpointOperation.CREATE: {Permission.CREATE},
73-
EndpointOperation.UPDATE: {Permission.UPDATE, Permission.CREATE},
74-
EndpointOperation.DELETE: {Permission.DELETE},
75-
EndpointOperation.SEARCH: {Permission.SEARCH},
76-
}
77-
78-
def _app_restricted(self, operation: EndpointOperation, aws_event: dict) -> None:
79-
allowed = self._parse_permissions(aws_event["headers"])
80-
requested = self._app_restricted_map[operation]
81-
if not requested.issubset(allowed):
38+
39+
if auth_type not in {AuthType.APP_RESTRICTED, AuthType.CIS2, AuthType.NHS_LOGIN}:
8240
raise UnauthorizedError()
8341

84-
def _cis2(self, operation: EndpointOperation, aws_event: dict) -> None:
85-
# Cis2 works exactly the same as ApplicationRestricted
86-
self._app_restricted(operation, aws_event)
87-
88-
@staticmethod
89-
def _parse_permissions(headers) -> Set[Permission]:
90-
"""Given headers return a set of Permissions. Raises UnknownPermission"""
91-
92-
content = headers.get(PERMISSIONS_HEADER, "")
93-
# comma-separate the Permissions header then trim and finally convert to lowercase
94-
parsed = [str.strip(str.lower(s)) for s in content.split(",")]
95-
96-
permissions = set()
97-
for s in parsed:
98-
try:
99-
permissions.add(Permission(s))
100-
except ValueError:
101-
raise UnknownPermission()
102-
103-
return permissions
104-
10542
@staticmethod
10643
def _parse_auth_type(headers) -> AuthType:
10744
try:
@@ -112,14 +49,13 @@ def _parse_auth_type(headers) -> AuthType:
11249
# we raise UnknownPermission in case of an error and not UnauthorizedError
11350
raise UnknownPermission()
11451

115-
116-
def authorize(operation: EndpointOperation):
52+
def authorize():
11753
def decorator(func):
11854
auth = Authorization()
11955

12056
@wraps(func)
12157
def wrapper(controller_instance, a):
122-
auth.authorize(operation, a)
58+
auth.authorize(a)
12359
return func(controller_instance, a)
12460

12561
return wrapper

backend/src/create_imms_handler.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pprint
44
import uuid
55

6-
from authorization import Permission
6+
77
from fhir_controller import FhirController, make_controller
88
from local_lambda import load_string
99
from models.errors import Severity, Code, create_operation_outcome
@@ -42,7 +42,6 @@ def create_immunization(event, controller: FhirController):
4242
"headers": {
4343
"Content-Type": "application/x-www-form-urlencoded",
4444
"AuthenticationType": "ApplicationRestricted",
45-
"Permissions": (",".join([Permission.CREATE])),
4645
},
4746
}
4847

backend/src/delete_imms_handler.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pprint
44
import uuid
55

6-
from authorization import Permission
6+
77
from fhir_controller import FhirController, make_controller
88
from models.errors import Severity, Code, create_operation_outcome
99
from log_structure import function_info
@@ -40,8 +40,7 @@ def delete_immunization(event, controller: FhirController):
4040
"pathParameters": {"id": args.id},
4141
"headers": {
4242
"Content-Type": "application/x-www-form-urlencoded",
43-
"AuthenticationType": "ApplicationRestricted",
44-
"Permissions": (",".join([Permission.DELETE])),
43+
"AuthenticationType": "ApplicationRestricted"
4544
},
4645
}
4746
pprint.pprint(delete_imms_handler(event, {}))

backend/src/fhir_controller.py

Lines changed: 18 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from fhir.resources.R4B.immunization import Immunization
1515
from boto3 import client as boto3_client
1616

17-
from authorization import Authorization, EndpointOperation, UnknownPermission
17+
from authorization import Authorization, UnknownPermission
1818
from cache import Cache
1919
from fhir_repository import ImmunizationRepository, create_table
2020
from fhir_service import FhirService, UpdateOutcome, get_service_url
@@ -80,7 +80,7 @@ def __init__(
8080
def get_immunization_by_identifier(self, aws_event) -> dict:
8181
try:
8282
if aws_event.get("headers"):
83-
if response := self.authorize_request(EndpointOperation.SEARCH, aws_event):
83+
if response := self.authorize_request(aws_event):
8484
return response
8585
query_params = aws_event.get("queryStringParameters", {})
8686
else:
@@ -121,7 +121,7 @@ def get_immunization_by_identifier(self, aws_event) -> dict:
121121
return self.create_response(403, unauthorized.to_operation_outcome())
122122

123123
def get_immunization_by_id(self, aws_event) -> dict:
124-
if response := self.authorize_request(EndpointOperation.READ, aws_event):
124+
if response := self.authorize_request(aws_event):
125125
return response
126126

127127
imms_id = aws_event["pathParameters"]["id"]
@@ -166,7 +166,7 @@ def get_immunization_by_id(self, aws_event) -> dict:
166166
def create_immunization(self, aws_event):
167167
try:
168168
if aws_event.get("headers"):
169-
if response := self.authorize_request(EndpointOperation.CREATE, aws_event):
169+
if response := self.authorize_request(aws_event):
170170
return response
171171
else:
172172
raise UnauthorizedError()
@@ -210,7 +210,7 @@ def create_immunization(self, aws_event):
210210
def update_immunization(self, aws_event):
211211
try:
212212
if aws_event.get("headers"):
213-
if response := self.authorize_request(EndpointOperation.UPDATE, aws_event):
213+
if response := self.authorize_request(aws_event):
214214
return response
215215
imms_id = aws_event["pathParameters"]["id"]
216216
else:
@@ -372,7 +372,7 @@ def update_immunization(self, aws_event):
372372
def delete_immunization(self, aws_event):
373373
try:
374374
if aws_event.get("headers"):
375-
if response := self.authorize_request(EndpointOperation.DELETE, aws_event):
375+
if response := self.authorize_request(aws_event):
376376
return response
377377
imms_id = aws_event["pathParameters"]["id"]
378378
else:
@@ -403,7 +403,7 @@ def delete_immunization(self, aws_event):
403403
return self.create_response(403, unauthorized.to_operation_outcome())
404404

405405
def search_immunizations(self, aws_event: APIGatewayProxyEventV1) -> dict:
406-
if response := self.authorize_request(EndpointOperation.SEARCH, aws_event):
406+
if response := self.authorize_request(aws_event):
407407
return response
408408

409409
try:
@@ -430,7 +430,9 @@ def search_immunizations(self, aws_event: APIGatewayProxyEventV1) -> dict:
430430
try:
431431
checker = VaccinePermissionChecker(imms_vax_type_perms)
432432
vax_type_perms = checker.expanded_permissions
433-
vax_type_perm = self._new_vaccine_request(search_params.immunization_targets, "search", vax_type_perms)
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 ]
434436
if not vax_type_perm:
435437
raise UnauthorizedVaxError
436438
except UnauthorizedVaxError as unauthorized:
@@ -541,19 +543,19 @@ def _create_bad_request(self, message):
541543
)
542544
return self.create_response(400, error)
543545

544-
def authorize_request(self, operation: EndpointOperation, aws_event: dict) -> Optional[dict]:
546+
547+
def authorize_request(self, aws_event: dict) -> Optional[dict]:
545548
try:
546-
self.authorizer.authorize(operation, aws_event)
549+
self.authorizer.authorize(aws_event)
547550
except UnauthorizedError as e:
548551
return self.create_response(403, e.to_operation_outcome())
549552
except UnknownPermission:
550-
# TODO: I think when AuthenticationType is not present, then we don't get below message. Double check again
551553
id_error = create_operation_outcome(
552-
resource_id=str(uuid.uuid4()),
553-
severity=Severity.error,
554-
code=Code.server_error,
555-
diagnostics="application includes invalid authorization values",
556-
)
554+
resource_id=str(uuid.uuid4()),
555+
severity=Severity.error,
556+
code=Code.server_error,
557+
diagnostics="Application includes invalid authorization values",
558+
)
557559
return self.create_response(500, id_error)
558560

559561
def fetch_identifier_system_and_element(self, event: dict):
@@ -663,37 +665,6 @@ def create_response(status_code, body=None, headers=None):
663665
**({"body": body} if body else {}),
664666
}
665667

666-
@staticmethod
667-
def _vaccine_permission(vaccine_type, operation) -> set:
668-
669-
operation = mappedOperation.mapped_operations.get(operation.lower())
670-
if not operation:
671-
raise ValueError(f"Unsupported operation: {operation}")
672-
673-
vaccine_permission = set()
674-
if isinstance(vaccine_type, list):
675-
for x in vaccine_type:
676-
vaccine_permission.add(str.lower(f"{x}.{operation}"))
677-
return vaccine_permission
678-
else:
679-
vaccine_permission.add(str.lower(f"{vaccine_type}.{operation}"))
680-
return vaccine_permission
681-
682-
@staticmethod
683-
def _new_vaccine_request(vaccine_type, operation, vaccine_type_permissions: None) -> Optional[list]:
684-
685-
operation = VaccinePermissionChecker.mapped_operations.get(operation.lower())
686-
vaccine_permission = list()
687-
if isinstance(vaccine_type, list):
688-
for x in vaccine_type:
689-
vaccs_prms = set()
690-
vaccs_prms.add(str.lower(f"{x}.{operation}"))
691-
if vaccs_prms.issubset(vaccine_type_permissions):
692-
vaccine_permission.append(x)
693-
return vaccine_permission
694-
else:
695-
return vaccine_permission
696-
697668
@staticmethod
698669
def _identify_supplier_system(aws_event):
699670
supplier_system = aws_event["headers"]["SupplierSystem"]

backend/src/fhir_service.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def __init__(
5858
self.validator = validator
5959

6060
def get_immunization_by_identifier(
61-
self, identifier_pk: str, imms_vax_type_perms: str, identifier: str, element: str
61+
self, identifier_pk: str, imms_vax_type_perms: list[str], identifier: str, element: str
6262
) -> Optional[dict]:
6363
"""
6464
Get an Immunization by its ID. Return None if not found. If the patient doesn't have an NHS number,
@@ -76,7 +76,7 @@ def get_immunization_by_identifier(
7676
response = form_json(imms_resp, element, identifier, base_url)
7777
return response
7878

79-
def get_immunization_by_id(self, imms_id: str, imms_vax_type_perms: str) -> Optional[dict]:
79+
def get_immunization_by_id(self, imms_id: str, imms_vax_type_perms: list[str]) -> Optional[dict]:
8080
"""
8181
Get an Immunization by its ID. Return None if it is not found. If the patient doesn't have an NHS number,
8282
return the Immunization without calling PDS or checking S flag.
@@ -155,7 +155,7 @@ def reinstate_immunization(
155155
imms_id: str,
156156
immunization: dict,
157157
existing_resource_version: int,
158-
imms_vax_type_perms: str,
158+
imms_vax_type_perms: list[str],
159159
supplier_system: str,
160160
) -> tuple[UpdateOutcome, Immunization]:
161161
immunization["id"] = imms_id
@@ -178,7 +178,7 @@ def update_reinstated_immunization(
178178
imms_id: str,
179179
immunization: dict,
180180
existing_resource_version: int,
181-
imms_vax_type_perms: str,
181+
imms_vax_type_perms: list[str],
182182
supplier_system: str,
183183
) -> tuple[UpdateOutcome, Immunization]:
184184
immunization["id"] = imms_id

backend/src/get_imms_handler.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pprint
44
import uuid
55

6-
from authorization import Permission
6+
77
from fhir_controller import FhirController, make_controller
88
from models.errors import Severity, Code, create_operation_outcome
99
from log_structure import function_info
@@ -41,7 +41,6 @@ def get_immunization_by_id(event, controller: FhirController):
4141
"headers": {
4242
"Content-Type": "application/x-www-form-urlencoded",
4343
"AuthenticationType": "ApplicationRestricted",
44-
"Permissions": (",".join([Permission.READ])),
4544
},
4645
}
4746
pprint.pprint(get_imms_handler(event, {}))

backend/src/models/errors.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -233,16 +233,17 @@ def __str__(self):
233233
return self.message
234234

235235

236-
@dataclass
237236
class UnauthorizedSystemError(RuntimeError):
238-
@staticmethod
239-
def to_operation_outcome() -> dict:
240-
msg = f"Unauthorized system"
237+
def __init__(self, message="Unauthorized system"):
238+
super().__init__(message)
239+
self.message = message
240+
241+
def to_operation_outcome(self) -> dict:
241242
return create_operation_outcome(
242243
resource_id=str(uuid.uuid4()),
243244
severity=Severity.error,
244245
code=Code.forbidden,
245-
diagnostics=msg,
246+
diagnostics=self.message,
246247
)
247248

248249

0 commit comments

Comments
 (0)