Skip to content

Commit aa9e0c9

Browse files
Akol125mfjarvis
andauthored
VED-26: Use configurable supplier permissions in the API. (#606)
* setup * replace vaccineTypePermissions * remove imms-batch-app * test for new perms and imms batch removel * test still * test3 * test_for_search * test for update * test for update2 * test suite complete * remove perm config in test * poetry * remove unused files * env vars * VED-26: Attach endpoint Lambda functions to the VPC so they can connect to Redis. * VED-26: Give endpoint Lambdas the required permissions to attach to the VPC. * VED-26: Try creating a fresh client for every invocation. * VED-26: Src again :( * VED-26: Revert Redis client change. Temporarily skip PDS callout. * VED-26: Revert PDS callout removal. * integrating the new supplier permissions * clean up * clean up2 * test suite * lint issues * refactor control flow and duplication error * e2e fix * e2e lint * refactor some logic from review sections * refactor from review section * lint issues * update * sanity check * VED-26: Attach Lambda functions and ECS tasks to private subnets only. * VED-26: Resolve Sonar warning. Add pre-deploy check for private subnets. * final review * final review2 --------- Co-authored-by: Matt Jarvis <[email protected]>
1 parent 9eed7ce commit aa9e0c9

36 files changed

+959
-1604
lines changed

backend/poetry.lock

Lines changed: 37 additions & 48 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ python = "~3.11"
1212
boto3 = "~1.38.42"
1313
boto3-stubs-lite = {extras = ["dynamodb"], version = "~1.38.42"}
1414
aws-lambda-typing = "~2.20.0"
15+
redis = "^4.6.0"
1516
moto = "^5.1.6"
1617
requests = "~2.32.4"
1718
responses = "~0.25.7"

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/clients.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
1-
"""Initialise s3, kinesis and lambda clients"""
1+
"""Initialise s3, kinesis, lambda and redis clients"""
22

33
from boto3 import client as boto3_client
4+
import os
5+
import logging
6+
import redis
47

5-
REGION_NAME = "eu-west-2"
8+
REGION_NAME = os.getenv("AWS_REGION", "eu-west-2")
69

710
s3_client = boto3_client("s3", region_name=REGION_NAME)
811
kinesis_client = boto3_client("kinesis", region_name=REGION_NAME)
912
lambda_client = boto3_client("lambda", region_name=REGION_NAME)
1013
firehose_client = boto3_client("firehose", region_name=REGION_NAME)
1114
sqs_client = boto3_client("sqs", region_name=REGION_NAME)
15+
16+
REDIS_HOST = os.getenv("REDIS_HOST", "")
17+
REDIS_PORT = int(os.getenv("REDIS_PORT", 6379))
18+
19+
20+
logging.basicConfig(level="INFO")
21+
logger = logging.getLogger()
22+
logger.info(f"Connecting to Redis at {REDIS_HOST}:{REDIS_PORT}")
23+
24+
redis_client = redis.StrictRedis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)

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, {}))

0 commit comments

Comments
 (0)