Skip to content

Commit 9c9a2a7

Browse files
authored
[NDR-344] Obtain a list of common names from an SSM parameter. (#972)
1 parent c308374 commit 9c9a2a7

9 files changed

+176
-16
lines changed

lambdas/enums/environment.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import os
2+
from enum import Enum
3+
4+
5+
class Environment(str, Enum):
6+
PROD = "prod"
7+
PRE_PROD = "pre-prod"
8+
NDR_TEST = "ndr-test"
9+
NDR_DEV = "ndr-dev"
10+
11+
@classmethod
12+
def from_env(cls) -> "Environment":
13+
value = os.getenv("WORKSPACE")
14+
if not value:
15+
return cls.NDR_DEV
16+
17+
return cls._value2member_map_.get(value.lower(), cls.NDR_DEV)

lambdas/enums/mtls.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
import boto3
2+
import json
3+
14
from enum import StrEnum, auto
5+
from functools import lru_cache
26

37
from enums.lambda_error import LambdaError
8+
from enums.environment import Environment
49
from utils.audit_logging_setup import LoggingService
510
from utils.lambda_exceptions import InvalidDocTypeException
611

@@ -12,13 +17,8 @@ class MtlsCommonNames(StrEnum):
1217

1318
@classmethod
1419
def allowed_names(cls) -> dict["MtlsCommonNames", list[str]]:
15-
return {
16-
cls.PDM: [
17-
"ndrclient.main.int.pdm.national.nhs.uk",
18-
"client.dev.ndr.national.nhs.uk",
19-
"client.preprod.ndr.national.nhs.uk"
20-
]
21-
}
20+
raw = cls._get_mtls_common_names()
21+
return {cls[k]: v for k, v in raw.items() if k in cls.__members__}
2222

2323
@classmethod
2424
def from_common_name(cls, common_name: str) -> "MtlsCommonNames | None":
@@ -27,3 +27,14 @@ def from_common_name(cls, common_name: str) -> "MtlsCommonNames | None":
2727
return doc_type
2828
logger.error(f"mTLS common name {common_name} - is not supported")
2929
raise InvalidDocTypeException(400, LambdaError.DocTypeInvalid)
30+
31+
@classmethod
32+
@lru_cache(maxsize=1)
33+
def _get_mtls_common_names(cls) -> dict[str, list[str]]:
34+
ssm = boto3.client("ssm")
35+
environment = Environment.from_env().value
36+
response = ssm.get_parameter(
37+
Name=f"/ndr/{environment}/mtls_common_names",
38+
WithDecryption=True,
39+
)
40+
return json.loads(response["Parameter"]["Value"])
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pytest
2+
3+
from enums.environment import Environment
4+
5+
6+
@pytest.mark.parametrize(
7+
"env_value, expected",
8+
[
9+
("prod", Environment.PROD),
10+
("pre-prod", Environment.PRE_PROD),
11+
("ndr-test", Environment.NDR_TEST),
12+
("ndr-dev", Environment.NDR_DEV),
13+
],
14+
)
15+
def test_valid_workspace_values(monkeypatch, env_value, expected):
16+
monkeypatch.setenv("WORKSPACE", env_value)
17+
assert Environment.from_env() == expected
18+
19+
20+
@pytest.mark.parametrize(
21+
"env_value",
22+
[
23+
"abcd1",
24+
"ndr000",
25+
"prmp000",
26+
"foobar",
27+
],
28+
)
29+
def test_invalid_workspace_defaults_to_ndr_dev(monkeypatch, env_value):
30+
monkeypatch.setenv("WORKSPACE", env_value)
31+
assert Environment.from_env() == Environment.NDR_DEV
32+
33+
34+
def test_workspace_is_case_insensitive(monkeypatch):
35+
monkeypatch.setenv("WORKSPACE", "PRE-PROD")
36+
assert Environment.from_env() == Environment.PRE_PROD
37+
38+
39+
def test_workspace_not_set_defaults_to_ndr_dev(monkeypatch):
40+
monkeypatch.delenv("WORKSPACE", raising=False)
41+
assert Environment.from_env() == Environment.NDR_DEV
42+
43+
44+
def test_workspace_empty_string_defaults_to_ndr_dev(monkeypatch):
45+
monkeypatch.setenv("WORKSPACE", "")
46+
assert Environment.from_env() == Environment.NDR_DEV

lambdas/tests/unit/enums/test_mtls.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@
77
@pytest.mark.parametrize(
88
["common_name", "expected"],
99
[
10-
("ndrclient.main.int.pdm.national.nhs.uk", MtlsCommonNames.PDM),
11-
("client.dev.ndr.national.nhs.uk", MtlsCommonNames.PDM),
10+
("xxx", MtlsCommonNames.PDM),
11+
("yyy", MtlsCommonNames.PDM),
12+
("zzz", MtlsCommonNames.PDM),
1213
],
1314
)
14-
def test_mtls_enum_returned(common_name, expected):
15+
def test_mtls_enum_returned(common_name, expected, monkeypatch):
16+
monkeypatch.setattr(
17+
MtlsCommonNames,
18+
"_get_mtls_common_names",
19+
classmethod(lambda cls: {"PDM": ["xxx", "yyy", "zzz"]}),
20+
)
1521
doc_type_enum = MtlsCommonNames.from_common_name(common_name)
1622
assert doc_type_enum == expected
1723

@@ -24,7 +30,12 @@ def test_mtls_enum_returned(common_name, expected):
2430
"foo.bar",
2531
],
2632
)
27-
def test_mtls_enum_error_raised(common_name):
33+
def test_mtls_enum_error_raised(common_name, monkeypatch):
34+
monkeypatch.setattr(
35+
MtlsCommonNames,
36+
"_get_mtls_common_names",
37+
classmethod(lambda cls: {"PDM": ["xxx", "yyy", "zzz"]}),
38+
)
2839
with pytest.raises(InvalidDocTypeException) as excinfo:
2940
MtlsCommonNames.from_common_name(common_name)
3041
assert excinfo.value.status_code == 400

lambdas/tests/unit/handlers/test_pdm_get_fhir_document_reference_by_id_handler.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
from enums.mtls import MtlsCommonNames
23
from enums.snomed_codes import SnomedCodes
34
from handlers.get_fhir_document_reference_handler import (
45
extract_document_parameters,
@@ -63,8 +64,18 @@ def mock_document_service(mocker):
6364
return mock_service_instance
6465

6566

67+
@pytest.fixture
68+
def mock_mtls_common_names(monkeypatch):
69+
monkeypatch.setattr(
70+
MtlsCommonNames,
71+
"_get_mtls_common_names",
72+
classmethod(lambda cls: {"PDM": ["ndrclient.main.int.pdm.national.nhs.uk"]}),
73+
)
74+
75+
6676
def test_lambda_handler_happy_path_with_mtls_pdm_login(
6777
set_env,
78+
mock_mtls_common_names,
6879
mock_document_service,
6980
context,
7081
):
@@ -85,7 +96,7 @@ def test_lambda_handler_happy_path_with_mtls_pdm_login(
8596
)
8697

8798

88-
def test_extract_bearer_token_when_pdm(context):
99+
def test_extract_bearer_token_when_pdm(context, mock_mtls_common_names):
89100
token = extract_bearer_token(MOCK_MTLS_VALID_EVENT, context)
90101
assert token is None
91102

lambdas/tests/unit/services/test_pdm_get_fhir_document_reference_search_service.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
from enums.mtls import MtlsCommonNames
23
from enums.snomed_codes import SnomedCodes
34
from freezegun import freeze_time
45
from models.document_reference import DocumentReference
@@ -44,6 +45,15 @@ def mock_filter_builder(mocker):
4445
return mock_filter
4546

4647

48+
@pytest.fixture
49+
def mock_mtls_common_names(monkeypatch):
50+
monkeypatch.setattr(
51+
MtlsCommonNames,
52+
"_get_mtls_common_names",
53+
classmethod(lambda cls: {"PDM": ["ndrclient.main.int.pdm.national.nhs.uk"]}),
54+
)
55+
56+
4757
@pytest.mark.parametrize(
4858
"common_name, expected",
4959
[
@@ -72,7 +82,9 @@ def mock_filter_builder(mocker):
7282
({}, ["test_pdm_dynamoDB_table", "test_lg_dynamoDB_table"]),
7383
],
7484
)
75-
def test_get_pdm_table(set_env, mock_document_service, common_name, expected):
85+
def test_get_pdm_table(
86+
set_env, mock_document_service, common_name, expected, mock_mtls_common_names
87+
):
7688
cn = validate_common_name_in_mtls(common_name)
7789
tables = mock_document_service._get_table_names(cn)
7890
assert tables == expected

lambdas/tests/unit/services/test_pdm_post_fhir_document_reference_service.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ def mock_fhir_doc_ref_base_service(mocker, setup_request_context):
6060
yield service
6161

6262

63+
@pytest.fixture
64+
def mock_mtls_common_names(monkeypatch):
65+
monkeypatch.setattr(
66+
MtlsCommonNames,
67+
"_get_mtls_common_names",
68+
classmethod(
69+
lambda cls: {
70+
"PDM": [
71+
"ndrclient.main.int.pdm.national.nhs.uk",
72+
"client.dev.ndr.national.nhs.uk",
73+
]
74+
}
75+
),
76+
)
77+
78+
6379
@pytest.fixture
6480
def setup_request_context():
6581
request_context.authorization = {
@@ -322,6 +338,7 @@ def test_get_dynamo_table_for_lloyd_george_doc_type(
322338
def test_process_mtls_fhir_document_reference_with_binary(
323339
mock_fhir_doc_ref_base_service,
324340
mock_post_fhir_doc_ref_service,
341+
mock_mtls_common_names,
325342
valid_mtls_fhir_doc_with_binary,
326343
valid_mtls_request_context,
327344
):

lambdas/tests/unit/utils/test_lambda_handler_utils.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
from enums.lambda_error import LambdaError
3+
from enums.mtls import MtlsCommonNames
34
from enums.snomed_codes import SnomedCodes
45
from tests.unit.conftest import TEST_UUID
56
from utils.lambda_exceptions import (
@@ -74,6 +75,22 @@
7475
]
7576

7677

78+
@pytest.fixture
79+
def mock_mtls_common_names(monkeypatch):
80+
monkeypatch.setattr(
81+
MtlsCommonNames,
82+
"_get_mtls_common_names",
83+
classmethod(
84+
lambda cls: {
85+
"PDM": [
86+
"ndrclient.main.int.pdm.national.nhs.uk",
87+
"client.dev.ndr.national.nhs.uk",
88+
]
89+
}
90+
),
91+
)
92+
93+
7794
@pytest.mark.parametrize(
7895
"function_name, mock_event",
7996
[
@@ -88,7 +105,7 @@ def test_extract_bearer_token_happy_paths(context, function_name, mock_event):
88105
assert token == f"Bearer {TEST_UUID}"
89106

90107

91-
def test_extract_bearer_token_when_pdm(context):
108+
def test_extract_bearer_token_when_pdm(context, mock_mtls_common_names):
92109
token = extract_bearer_token(MOCK_MTLS_VALID_EVENT, context)
93110
assert token is None
94111

lambdas/tests/unit/utils/test_lambda_header_utils.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,32 @@ def invalid_mtls_request_context():
6464
}
6565

6666

67-
def test_validate_valid_common_name(valid_mtls_request_context):
67+
@pytest.fixture
68+
def mock_mtls_common_names(monkeypatch):
69+
monkeypatch.setattr(
70+
MtlsCommonNames,
71+
"_get_mtls_common_names",
72+
classmethod(
73+
lambda cls: {
74+
"PDM": [
75+
"ndrclient.main.int.pdm.national.nhs.uk",
76+
"client.dev.ndr.national.nhs.uk",
77+
]
78+
}
79+
),
80+
)
81+
82+
83+
def test_validate_valid_common_name(valid_mtls_request_context, mock_mtls_common_names):
6884
"""Test validate_common_name when mtls and pdm."""
6985
result = validate_common_name_in_mtls(valid_mtls_request_context)
7086

7187
assert result == MtlsCommonNames.PDM.value
7288

7389

74-
def test_validate_invalid_common_name(invalid_mtls_request_context):
90+
def test_validate_invalid_common_name(
91+
invalid_mtls_request_context, mock_mtls_common_names
92+
):
7593
"""Test validate_common_name when mtls but not allowed."""
7694
with pytest.raises(InvalidDocTypeException) as excinfo:
7795
validate_common_name_in_mtls(invalid_mtls_request_context)

0 commit comments

Comments
 (0)