From 34f6509f065047d108a935263472c91442312ed7 Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Wed, 15 Oct 2025 13:10:13 +0100 Subject: [PATCH 1/2] [NRL-1700] Log info about which clientcert is being used for each request --- layer/nrlf/core/decorators.py | 14 +++++++++++--- layer/nrlf/tests/events.py | 7 +++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/layer/nrlf/core/decorators.py b/layer/nrlf/core/decorators.py index 683ca3e00..f2dc724db 100644 --- a/layer/nrlf/core/decorators.py +++ b/layer/nrlf/core/decorators.py @@ -236,15 +236,23 @@ def request_handler( """ def wrapped_func(func: RequestHandler): - def wrapper(*args, **kwargs): - event: APIGatewayProxyEvent = args[0] - context: LambdaContext = args[1] + def wrapper(event: APIGatewayProxyEvent, context: LambdaContext, **kwargs): + client_cert = event.request_context.identity.client_cert + if client_cert: + client_cert_info = { + "subject_dn": client_cert.subject_dn, + "issuer_dn": client_cert.issuer_dn, + "serial_number": client_cert.serial_number, + } + else: + client_cert_info = "No client certificate provided" logger.log( code=LogReference.HANDLER000, method=event.http_method, path=event.path, headers=event.headers, + client_cert_info=client_cert_info, ) if skip_request_verification: diff --git a/layer/nrlf/tests/events.py b/layer/nrlf/tests/events.py index c32d7cd31..7bc4f0a26 100644 --- a/layer/nrlf/tests/events.py +++ b/layer/nrlf/tests/events.py @@ -55,6 +55,13 @@ def create_test_api_gateway_event( "resourcePath": "/", "httpMethod": "GET", "path": "/Prod/", + "identity": { + "client_cert": { + "subject_dn": "CN=TEST SUBJECT", + "issuer_dn": "CN=TEST ISSUER", + "serial_number": "0000001", + } + }, }, "headers": headers or create_headers(), "multiValueHeaders": {}, From 4dc64573beb85e656df4b93a458339441493cdaf Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Wed, 22 Oct 2025 16:21:51 +0100 Subject: [PATCH 2/2] [NRL-1700] Add unit tests for client_cert info logging --- layer/nrlf/core/tests/test_decorators.py | 69 ++++++++++++++++++++++++ layer/nrlf/tests/events.py | 8 +-- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/layer/nrlf/core/tests/test_decorators.py b/layer/nrlf/core/tests/test_decorators.py index ea9d65717..9188a0e47 100644 --- a/layer/nrlf/core/tests/test_decorators.py +++ b/layer/nrlf/core/tests/test_decorators.py @@ -1,5 +1,6 @@ import json import warnings +from typing import Any import pytest from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent @@ -288,6 +289,74 @@ def decorated_function(event): ) +def test_log_includes_client_cert_details(mocker: MockerFixture): + @request_handler() + def decorated_function() -> Response: + return Response( + statusCode="200", + body=json.dumps({"message": "Hello, World!"}), + headers={"Content-Type": "application/json"}, + ) + + test_event = create_test_api_gateway_event() + event = APIGatewayProxyEvent(test_event) + + mock_logger = mocker.patch("nrlf.core.decorators.logger") + + decorated_function(event, create_mock_context()) + + assert any( + call[1]["code"].name == "HANDLER000" + for call in mock_logger.log.call_args_list + if call[1] + ) + + logged_cert_info: dict[str, Any] = [ + call[1:][0] + for call in mock_logger.log.call_args_list + if call[1] and "code" in call[1] and call[1]["code"].name == "HANDLER000" + ][0]["client_cert_info"] + + client_cert = event.request_context.identity.client_cert + assert logged_cert_info == { + "subject_dn": client_cert.subject_dn, + "issuer_dn": client_cert.issuer_dn, + "serial_number": client_cert.serial_number, + } + + +def test_log_includes_client_cert_details_when_no_cert(mocker: MockerFixture): + @request_handler() + def decorated_function() -> Response: + return Response( + statusCode="200", + body=json.dumps({"message": "Hello, World!"}), + headers={"Content-Type": "application/json"}, + ) + + test_event = create_test_api_gateway_event() + test_event["requestContext"]["identity"]["clientCert"] = None + event = APIGatewayProxyEvent(test_event) + + mock_logger = mocker.patch("nrlf.core.decorators.logger") + + decorated_function(event, create_mock_context()) + + assert any( + call[1]["code"].name == "HANDLER000" + for call in mock_logger.log.call_args_list + if call[1] + ) + + logged_cert_info: dict[str, Any] = [ + call[1:][0] + for call in mock_logger.log.call_args_list + if call[1] and "code" in call[1] and call[1]["code"].name == "HANDLER000" + ][0]["client_cert_info"] + + assert logged_cert_info == "No client certificate provided" + + def test_verify_request_id_happy_path(): test_event = create_test_api_gateway_event() diff --git a/layer/nrlf/tests/events.py b/layer/nrlf/tests/events.py index 7bc4f0a26..003fa87c5 100644 --- a/layer/nrlf/tests/events.py +++ b/layer/nrlf/tests/events.py @@ -56,10 +56,10 @@ def create_test_api_gateway_event( "httpMethod": "GET", "path": "/Prod/", "identity": { - "client_cert": { - "subject_dn": "CN=TEST SUBJECT", - "issuer_dn": "CN=TEST ISSUER", - "serial_number": "0000001", + "clientCert": { + "subjectDN": "CN=TEST SUBJECT", + "issuerDN": "CN=TEST ISSUER", + "serialNumber": "0000001", } }, },