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/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 c32d7cd31..003fa87c5 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": { + "clientCert": { + "subjectDN": "CN=TEST SUBJECT", + "issuerDN": "CN=TEST ISSUER", + "serialNumber": "0000001", + } + }, }, "headers": headers or create_headers(), "multiValueHeaders": {},