Skip to content

Commit d812d85

Browse files
Merge pull request #1172 from NHSDigital/NRL-2015-option-4
NRL-2015 option 4 - choose v2 if v2 permissions file in lambda layer
2 parents 2670204 + 644137f commit d812d85

30 files changed

Lines changed: 540 additions & 281 deletions

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ SHELL := /bin/bash
88
DIST_PATH ?= ./dist
99
TEST_ARGS ?= --cov --cov-report=term-missing --cov-report=xml:$(DIST_PATH)/test-coverage.xml
1010
SMOKE_TEST_ARGS ?=
11-
FEATURE_TEST_ARGS ?= ./tests/features --format progress2
11+
FEATURE_TEST_ARGS ?= ./tests/features
1212
TF_WORKSPACE_NAME ?= $(shell terraform -chdir=terraform/infrastructure workspace show)
1313
ENV ?= dev
1414
ACCOUNT ?= dev
@@ -117,6 +117,7 @@ test-features-integration: check-warn ## Run the BDD feature tests in the integr
117117
--define="env=$(TF_WORKSPACE_NAME)" \
118118
--define="account_name=$(ENV)" \
119119
--define="use_shared_resources=${USE_SHARED_RESOURCES}" \
120+
-v --format progress2 \
120121
$(FEATURE_TEST_ARGS)
121122

122123
integration-test-with-custom_tag:

layer/nrlf/core/authoriser.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,24 @@ def get_pointer_permissions_v2(
2525
# check for app-wide permissions
2626
app_wide_key = f"{producer_or_consumer}/{app_id}.json"
2727
if path.isfile(f"/opt/python/nrlf_permissions/{app_wide_key}"):
28-
logger.log(LogReference.V2PERMISSIONS011, key=app_wide_key)
2928
key = app_wide_key
3029
else: # use org level
3130
key = f"{producer_or_consumer}/{app_id}/{ods_code}.json"
32-
logger.log(LogReference.V2PERMISSIONS011, key=key)
31+
32+
logger.log(LogReference.V2PERMISSIONS011, key=key)
3333
file_path = f"/opt/python/nrlf_permissions/{key}"
3434

3535
pointer_permissions = {}
3636
try:
3737
with open(file_path) as file:
3838
pointer_permissions = json.load(file)
39+
except FileNotFoundError as exc:
40+
logger.log(
41+
LogReference.V2PERMISSIONS013,
42+
exc_info=sys.exc_info(),
43+
error=str(exc),
44+
)
45+
raise exc
3946
except Exception as exc:
4047
logger.log(
4148
LogReference.S3PERMISSIONS005,

layer/nrlf/core/decorators.py

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
event_source,
1010
)
1111
from aws_lambda_powertools.utilities.typing import LambdaContext
12-
from pydantic import BaseModel
12+
from pydantic import BaseModel, ValidationError
1313

1414
from nrlf.core.authoriser import (
1515
get_pointer_permissions_v2,
@@ -19,8 +19,6 @@
1919
from nrlf.core.codes import SpineErrorConcept
2020
from nrlf.core.config import Config
2121
from nrlf.core.constants import (
22-
CLIENT_RP_DETAILS,
23-
CONNECTION_METADATA,
2422
NHSD_CORRELATION_ID_HEADER,
2523
PERMISSION_ALLOW_ALL_POINTER_TYPES,
2624
X_CORRELATION_ID_HEADER,
@@ -31,7 +29,7 @@
3129
from nrlf.core.dynamodb.repository import DocumentPointerRepository
3230
from nrlf.core.errors import OperationOutcomeError, ParseError
3331
from nrlf.core.logger import LogReference, logger
34-
from nrlf.core.model import PermissionsPolicy
32+
from nrlf.core.model import ConnectionMetadata, PermissionsPolicy
3533
from nrlf.core.request import parse_body, parse_headers, parse_params, parse_path
3634
from nrlf.core.response import Response
3735

@@ -74,7 +72,7 @@ def wrapper(*args, **kwargs) -> Dict[str, Any]:
7472

7573

7674
def header_handler(
77-
wrapped_func: Callable[..., Dict[str, Any]]
75+
wrapped_func: Callable[..., Dict[str, Any]],
7876
) -> Callable[..., Dict[str, Any]]:
7977
"""
8078
Wraps the function to set the specific headers in the request and response
@@ -118,7 +116,7 @@ def wrapper(*args, **kwargs) -> Dict[str, Any]:
118116

119117

120118
def logger_initialiser(
121-
wrapper_func: Callable[..., Dict[str, Any]]
119+
wrapper_func: Callable[..., Dict[str, Any]],
122120
) -> Callable[..., Dict[str, Any]]:
123121
"""
124122
Wraps the function and initialises the request logger
@@ -145,27 +143,48 @@ def wrapper(*args, **kwargs) -> Dict[str, Any]:
145143
RepositoryType = Union[Type[DocumentPointerRepository], None]
146144

147145

148-
def _use_v2_permissions_model(headers: Dict[str, str]) -> bool:
149-
case_insensitive_headers = {key.lower(): value for key, value in headers.items()}
150-
# if either or both headers are missing
151-
return (
152-
CLIENT_RP_DETAILS not in case_insensitive_headers.keys()
153-
or CONNECTION_METADATA not in case_insensitive_headers.keys()
154-
)
146+
def v1_perms_stuff(metadata: ConnectionMetadata, config: Config):
147+
if PERMISSION_ALLOW_ALL_POINTER_TYPES in metadata.nrl_permissions:
148+
logger.log(LogReference.HANDLER004a)
149+
metadata.pointer_types = PointerTypes.list()
150+
return metadata
151+
152+
logger.log(LogReference.HANDLER004b)
153+
pointer_types = parse_permissions_file(metadata)
154+
if not pointer_types and not metadata.is_test_event:
155+
logger.log(LogReference.HANDLER004)
156+
pointer_types = get_pointer_types(metadata, config)
155157

158+
metadata.pointer_types = pointer_types
159+
logger.log(LogReference.HANDLER004c, pointer_types=pointer_types)
156160

157-
def _load_v2_connection_metadata(headers: Dict[str, str], path: str):
158-
logger.log(LogReference.HANDLER004d)
161+
return metadata
159162

160-
metadata = parse_headers(headers, use_v2_permissions=True)
161-
logger.log(LogReference.HANDLER003, metadata=metadata.model_dump())
162163

163-
logger.log(LogReference.HANDLER004b)
164+
def v2_perms_stuff(metadata: ConnectionMetadata, path=""):
164165
pointer_permissions = get_pointer_permissions_v2(metadata, path)
165166

166-
metadata.nrl_permissions_policy = PermissionsPolicy.model_validate(
167-
pointer_permissions
168-
)
167+
try:
168+
metadata.nrl_permissions_policy = PermissionsPolicy.model_validate(
169+
pointer_permissions
170+
)
171+
except ValidationError as err:
172+
logger.log(
173+
LogReference.HANDLER004e,
174+
pointer_permissions=pointer_permissions,
175+
path=path,
176+
validation_errors=err.errors(),
177+
)
178+
raise OperationOutcomeError(
179+
status_code="401",
180+
severity="error",
181+
code="invalid",
182+
details=SpineErrorConcept.from_code("MISSING_OR_INVALID_HEADER"),
183+
diagnostics=(
184+
"Unable to parse metadata about the requesting application. "
185+
"Contact the onboarding team."
186+
),
187+
) from None
169188

170189
if (
171190
AccessControls.ALLOW_ALL_TYPES.value
@@ -189,27 +208,16 @@ def _load_v2_connection_metadata(headers: Dict[str, str], path: str):
189208
def load_connection_metadata(headers: Dict[str, str], config: Config, path=""):
190209
logger.log(LogReference.HANDLER002, headers=headers)
191210

192-
if _use_v2_permissions_model(headers):
193-
return _load_v2_connection_metadata(headers, path)
194-
195-
metadata = parse_headers(headers, use_v2_permissions=False)
211+
metadata = parse_headers(headers)
196212
logger.log(LogReference.HANDLER003, metadata=metadata.model_dump())
197213

198-
if PERMISSION_ALLOW_ALL_POINTER_TYPES in metadata.nrl_permissions:
199-
logger.log(LogReference.HANDLER004a)
200-
metadata.pointer_types = PointerTypes.list()
201-
return metadata
202-
203-
logger.log(LogReference.HANDLER004b)
204-
pointer_types = parse_permissions_file(metadata)
205-
if not pointer_types and not metadata.is_test_event:
206-
logger.log(LogReference.HANDLER004)
207-
pointer_types = get_pointer_types(metadata, config)
208-
209-
metadata.pointer_types = pointer_types
210-
logger.log(LogReference.HANDLER004c, pointer_types=pointer_types)
214+
try:
215+
return v2_perms_stuff(metadata, path)
216+
except FileNotFoundError:
217+
# No v2 perms file found, so try v1 instead
218+
pass
211219

212-
return metadata
220+
return v1_perms_stuff(metadata, config)
213221

214222

215223
def filter_kwargs(handler_func: RequestHandler, kwargs: Dict[str, Any]):

layer/nrlf/core/log_references.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class LogReference(Enum):
3333
HANDLER004b = _Reference("INFO", "Parsing embedded permissions file")
3434
HANDLER004c = _Reference("INFO", "Parsed embedded permissions file")
3535
HANDLER004d = _Reference("INFO", "Using v2 permissions model")
36+
HANDLER004e = _Reference("ERROR", "Unable to validate PermissionsPolicy")
3637
HANDLER005 = _Reference("WARN", "Rejecting request due to missing pointer types")
3738
HANDLER006 = _Reference("DEBUG", "Attempting to parse request parameters")
3839
HANDLER007 = _Reference("INFO", "Parsed request parameters")
@@ -88,7 +89,7 @@ class LogReference(Enum):
8889
"INFO", "Retrieved v2 pointer permissions from lambda layer"
8990
)
9091
V2PERMISSIONS013 = _Reference(
91-
"WARN", "No v2 permissions file found in lambda layer"
92+
"INFO", "No v2 permissions file found in lambda layer"
9293
)
9394

9495
# Parse Logs

layer/nrlf/core/request.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@
1111
from nrlf.core.model import ClientRpDetails, ConnectionMetadata
1212

1313

14-
def _fetch_ods_app_id_headers(headers: dict[str, str]):
15-
14+
def _fetch_v2_ods_app_id_headers(headers: dict[str, str]):
1615
case_insensitive_headers = {key.lower(): value for key, value in headers.items()}
1716

1817
ods_code = case_insensitive_headers.get(V2Headers.NHSD_END_USER_ORGANISATION_ODS)
19-
2018
if not ods_code or len(ods_code.strip()) == 0:
2119
logger.log(
2220
LogReference.HANDLER003a,
@@ -33,9 +31,7 @@ def _fetch_ods_app_id_headers(headers: dict[str, str]):
3331
return ods_code, nrl_app_id
3432

3533

36-
def parse_headers(
37-
headers: Dict[str, str], use_v2_permissions=False
38-
) -> ConnectionMetadata:
34+
def parse_headers(headers: Dict[str, str]) -> ConnectionMetadata:
3935
"""
4036
Parses the connection metadata and client rp details from the headers passed from Apigee
4137
"""
@@ -49,8 +45,8 @@ def parse_headers(
4945
case_insensitive_headers.get(CONNECTION_METADATA, "{}")
5046
)
5147

52-
if use_v2_permissions:
53-
ods_code, nrl_app_id = _fetch_ods_app_id_headers(case_insensitive_headers)
48+
ods_code, nrl_app_id = _fetch_v2_ods_app_id_headers(case_insensitive_headers)
49+
if ods_code and nrl_app_id:
5450
raw_connection_metadata["nrl.ods-code"] = ods_code
5551
raw_connection_metadata["nrl.app-id"] = nrl_app_id
5652
raw_client_rp_details["developer.app.id"] = nrl_app_id

layer/nrlf/core/tests/test_authoriser.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from unittest.mock import mock_open, patch
22

3+
import pytest
4+
35
from nrlf.core.authoriser import get_pointer_permissions_v2, parse_permissions_file
46
from nrlf.core.logger import LogReference, logger
57
from nrlf.core.request import parse_headers
@@ -76,17 +78,16 @@ def test_authoriser_get_v2_permissions_with_app_pointer_types(
7678
spy.assert_called_with(LogReference.V2PERMISSIONS011, key=expected_lookup_key)
7779

7880

79-
def test_authoriser_parse_v2_permission_file_with_no_permission_file(mocker):
80-
spy = mocker.spy(logger, "log")
81-
expected_lookup_key = "consumer/NotAnApp/NotFound.json"
81+
def test_authoriser_parse_v2_permission_file_with_no_permission_file():
82+
with pytest.raises(FileNotFoundError) as error:
83+
get_pointer_permissions_v2(
84+
connection_metadata=parse_headers(
85+
create_headers(ods_code="NotFound", nrl_app_id="NotAnApp")
86+
),
87+
request_path="/consumer/_status",
88+
)
8289

83-
metadata_result = get_pointer_permissions_v2(
84-
connection_metadata=parse_headers(
85-
create_headers(ods_code="NotFound", nrl_app_id="NotAnApp")
86-
),
87-
request_path="/consumer/_status",
90+
assert (
91+
f"No such file or directory: '/opt/python/nrlf_permissions/consumer/NotAnApp/NotFound.json'"
92+
in str(error.value)
8893
)
89-
90-
assert metadata_result == {}
91-
92-
spy.assert_any_call(LogReference.V2PERMISSIONS011, key=expected_lookup_key)

0 commit comments

Comments
 (0)