From 8e026662c7da2ed82795a4af723451d231daba78 Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Mon, 3 Mar 2025 13:53:52 +0100 Subject: [PATCH 1/4] add validation for AWS ARN in PutIntegration --- .../services/apigateway/legacy/provider.py | 22 +++++- .../apigateway/test_apigateway_lambda.py | 74 +++++++++++++++++++ .../test_apigateway_lambda.snapshot.json | 49 ++++++++++++ .../test_apigateway_lambda.validation.json | 3 + 4 files changed, 144 insertions(+), 4 deletions(-) diff --git a/localstack-core/localstack/services/apigateway/legacy/provider.py b/localstack-core/localstack/services/apigateway/legacy/provider.py index 502051ed77e57..d21f42a18e5aa 100644 --- a/localstack-core/localstack/services/apigateway/legacy/provider.py +++ b/localstack-core/localstack/services/apigateway/legacy/provider.py @@ -122,7 +122,7 @@ from localstack.services.edge import ROUTER from localstack.services.moto import call_moto, call_moto_with_request from localstack.services.plugins import ServiceLifecycleHook -from localstack.utils.aws.arns import get_partition +from localstack.utils.aws.arns import InvalidArnException, get_partition, parse_arn from localstack.utils.collections import ( DelSafeDict, PaginatedList, @@ -1937,13 +1937,27 @@ def put_integration( f"Member must satisfy enum value set: [HTTP, MOCK, AWS_PROXY, HTTP_PROXY, AWS]", ) - elif integration_type == IntegrationType.AWS_PROXY: - integration_uri = request.get("uri") or "" - if ":lambda:" not in integration_uri and ":firehose:" not in integration_uri: + elif integration_type in (IntegrationType.AWS_PROXY, IntegrationType.AWS): + if not (integration_uri := request.get("uri") or "").startswith("arn:"): + raise BadRequestException("Invalid ARN specified in the request") + + try: + parsed_arn = parse_arn(integration_uri) + except InvalidArnException: + raise BadRequestException("Invalid ARN specified in the request") + + if not any( + parsed_arn["resource"].startswith(action_type) for action_type in ("path", "action") + ): + raise BadRequestException("AWS ARN for integration must contain path or action") + + if integration_type == IntegrationType.AWS_PROXY and parsed_arn["account"] != "lambda": + # the Firehose message is misleading, this is not implemented in AWS raise BadRequestException( "Integrations of type 'AWS_PROXY' currently only supports " "Lambda function and Firehose stream invocations." ) + moto_rest_api = get_moto_rest_api(context=context, rest_api_id=request.get("restApiId")) resource = moto_rest_api.resources.get(request.get("resourceId")) if not resource: diff --git a/tests/aws/services/apigateway/test_apigateway_lambda.py b/tests/aws/services/apigateway/test_apigateway_lambda.py index c4eebb9361939..e6ce1bc226d98 100644 --- a/tests/aws/services/apigateway/test_apigateway_lambda.py +++ b/tests/aws/services/apigateway/test_apigateway_lambda.py @@ -292,6 +292,80 @@ def invoke_api_with_multi_value_header(url): snapshot.match("invocation-hardcoded", response_hardcoded.json()) +@markers.aws.validated +def test_put_integration_aws_proxy_uri( + aws_client, + create_rest_apigw, + create_lambda_function, + create_role_with_policy, + snapshot, + region_name, +): + api_id, _, root_resource_id = create_rest_apigw( + name=f"test-api-{short_uid()}", + description="APIGW test PutIntegration AWS_PROXY URI", + ) + function_name = f"function-{short_uid()}" + + # create lambda + create_function_response = create_lambda_function( + func_name=function_name, + handler_file=TEST_LAMBDA_AWS_PROXY, + handler="lambda_aws_proxy.handler", + runtime=Runtime.python3_12, + ) + # create invocation role + _, role_arn = create_role_with_policy( + "Allow", "lambda:InvokeFunction", json.dumps(APIGATEWAY_ASSUME_ROLE_POLICY), "*" + ) + lambda_arn = create_function_response["CreateFunctionResponse"]["FunctionArn"] + + aws_client.apigateway.put_method( + restApiId=api_id, + resourceId=root_resource_id, + httpMethod="ANY", + authorizationType="NONE", + ) + + # f"arn:aws:apigateway:{region_name}:lambda:path/2015-03-31/functions/{lambda_arn}/invocations", + default_params = { + "restApiId": api_id, + "resourceId": root_resource_id, + "httpMethod": "ANY", + "type": "AWS_PROXY", + "integrationHttpMethod": "POST", + "credentials": role_arn, + } + + with pytest.raises(ClientError) as e: + aws_client.apigateway.put_integration( + **default_params, + uri=lambda_arn, + ) + snapshot.match("put-integration-lambda-uri", e.value.response) + + with pytest.raises(ClientError) as e: + aws_client.apigateway.put_integration( + **default_params, + uri=f"bad-arn:lambda:path/2015-03-31/functions/{lambda_arn}/invocations", + ) + snapshot.match("put-integration-wrong-arn", e.value.response) + + with pytest.raises(ClientError) as e: + aws_client.apigateway.put_integration( + **default_params, + uri=f"arn:aws:apigateway:{region_name}:lambda:test/2015-03-31/functions/{lambda_arn}/invocations", + ) + snapshot.match("put-integration-wrong-type", e.value.response) + + with pytest.raises(ClientError) as e: + aws_client.apigateway.put_integration( + **default_params, + uri=f"arn:aws:apigateway:{region_name}:firehose:path/2015-03-31/functions/{lambda_arn}/invocations", + ) + snapshot.match("put-integration-wrong-firehose", e.value.response) + + @markers.aws.validated def test_lambda_aws_proxy_integration_non_post_method( create_rest_apigw, create_lambda_function, create_role_with_policy, snapshot, aws_client diff --git a/tests/aws/services/apigateway/test_apigateway_lambda.snapshot.json b/tests/aws/services/apigateway/test_apigateway_lambda.snapshot.json index 1ff320770f0ad..c06735278d000 100644 --- a/tests/aws/services/apigateway/test_apigateway_lambda.snapshot.json +++ b/tests/aws/services/apigateway/test_apigateway_lambda.snapshot.json @@ -1795,5 +1795,54 @@ "content": "" } } + }, + "tests/aws/services/apigateway/test_apigateway_lambda.py::test_put_integration_aws_proxy_uri": { + "recorded-date": "03-03-2025, 12:39:42", + "recorded-content": { + "put-integration-lambda-uri": { + "Error": { + "Code": "BadRequestException", + "Message": "AWS ARN for integration must contain path or action" + }, + "message": "AWS ARN for integration must contain path or action", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "put-integration-wrong-arn": { + "Error": { + "Code": "BadRequestException", + "Message": "Invalid ARN specified in the request" + }, + "message": "Invalid ARN specified in the request", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "put-integration-wrong-type": { + "Error": { + "Code": "BadRequestException", + "Message": "AWS ARN for integration must contain path or action" + }, + "message": "AWS ARN for integration must contain path or action", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "put-integration-wrong-firehose": { + "Error": { + "Code": "BadRequestException", + "Message": "Integrations of type 'AWS_PROXY' currently only supports Lambda function and Firehose stream invocations." + }, + "message": "Integrations of type 'AWS_PROXY' currently only supports Lambda function and Firehose stream invocations.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } } } diff --git a/tests/aws/services/apigateway/test_apigateway_lambda.validation.json b/tests/aws/services/apigateway/test_apigateway_lambda.validation.json index 342622e819dc5..dc47f503dc8ab 100644 --- a/tests/aws/services/apigateway/test_apigateway_lambda.validation.json +++ b/tests/aws/services/apigateway/test_apigateway_lambda.validation.json @@ -31,5 +31,8 @@ }, "tests/aws/services/apigateway/test_apigateway_lambda.py::test_lambda_selection_patterns": { "last_validated_date": "2023-09-05T19:54:21+00:00" + }, + "tests/aws/services/apigateway/test_apigateway_lambda.py::test_put_integration_aws_proxy_uri": { + "last_validated_date": "2025-03-03T12:39:42+00:00" } } From 57f08393f28274f28a9063731445eaa91ade9816 Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Mon, 3 Mar 2025 14:01:34 +0100 Subject: [PATCH 2/4] add case --- .../services/apigateway/legacy/provider.py | 5 ++++- .../services/apigateway/test_apigateway_lambda.py | 8 +++++++- .../apigateway/test_apigateway_lambda.snapshot.json | 13 ++++++++++++- .../test_apigateway_lambda.validation.json | 2 +- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/localstack-core/localstack/services/apigateway/legacy/provider.py b/localstack-core/localstack/services/apigateway/legacy/provider.py index d21f42a18e5aa..dfb27be992d02 100644 --- a/localstack-core/localstack/services/apigateway/legacy/provider.py +++ b/localstack-core/localstack/services/apigateway/legacy/provider.py @@ -1951,7 +1951,10 @@ def put_integration( ): raise BadRequestException("AWS ARN for integration must contain path or action") - if integration_type == IntegrationType.AWS_PROXY and parsed_arn["account"] != "lambda": + if integration_type == IntegrationType.AWS_PROXY and ( + parsed_arn["account"] != "lambda" + or not parsed_arn["resource"].startswith("path/2015-03-31/functions/") + ): # the Firehose message is misleading, this is not implemented in AWS raise BadRequestException( "Integrations of type 'AWS_PROXY' currently only supports " diff --git a/tests/aws/services/apigateway/test_apigateway_lambda.py b/tests/aws/services/apigateway/test_apigateway_lambda.py index e6ce1bc226d98..c1b42d727a182 100644 --- a/tests/aws/services/apigateway/test_apigateway_lambda.py +++ b/tests/aws/services/apigateway/test_apigateway_lambda.py @@ -327,7 +327,6 @@ def test_put_integration_aws_proxy_uri( authorizationType="NONE", ) - # f"arn:aws:apigateway:{region_name}:lambda:path/2015-03-31/functions/{lambda_arn}/invocations", default_params = { "restApiId": api_id, "resourceId": root_resource_id, @@ -365,6 +364,13 @@ def test_put_integration_aws_proxy_uri( ) snapshot.match("put-integration-wrong-firehose", e.value.response) + with pytest.raises(ClientError) as e: + aws_client.apigateway.put_integration( + **default_params, + uri=f"arn:aws:apigateway:{region_name}:lambda:path/random/value/{lambda_arn}/invocations", + ) + snapshot.match("put-integration-bad-lambda-arn", e.value.response) + @markers.aws.validated def test_lambda_aws_proxy_integration_non_post_method( diff --git a/tests/aws/services/apigateway/test_apigateway_lambda.snapshot.json b/tests/aws/services/apigateway/test_apigateway_lambda.snapshot.json index c06735278d000..f91bd1cb104c2 100644 --- a/tests/aws/services/apigateway/test_apigateway_lambda.snapshot.json +++ b/tests/aws/services/apigateway/test_apigateway_lambda.snapshot.json @@ -1797,7 +1797,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_lambda.py::test_put_integration_aws_proxy_uri": { - "recorded-date": "03-03-2025, 12:39:42", + "recorded-date": "03-03-2025, 12:58:39", "recorded-content": { "put-integration-lambda-uri": { "Error": { @@ -1842,6 +1842,17 @@ "HTTPHeaders": {}, "HTTPStatusCode": 400 } + }, + "put-integration-bad-lambda-arn": { + "Error": { + "Code": "BadRequestException", + "Message": "Integrations of type 'AWS_PROXY' currently only supports Lambda function and Firehose stream invocations." + }, + "message": "Integrations of type 'AWS_PROXY' currently only supports Lambda function and Firehose stream invocations.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } } } } diff --git a/tests/aws/services/apigateway/test_apigateway_lambda.validation.json b/tests/aws/services/apigateway/test_apigateway_lambda.validation.json index dc47f503dc8ab..8dcc1e29b6fc8 100644 --- a/tests/aws/services/apigateway/test_apigateway_lambda.validation.json +++ b/tests/aws/services/apigateway/test_apigateway_lambda.validation.json @@ -33,6 +33,6 @@ "last_validated_date": "2023-09-05T19:54:21+00:00" }, "tests/aws/services/apigateway/test_apigateway_lambda.py::test_put_integration_aws_proxy_uri": { - "last_validated_date": "2025-03-03T12:39:42+00:00" + "last_validated_date": "2025-03-03T12:58:39+00:00" } } From e29799d8368dcf1aeba93d2145ae20f63b9a246a Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Mon, 3 Mar 2025 14:28:00 +0100 Subject: [PATCH 3/4] move validation before moto to have right validation order --- .../localstack/services/apigateway/legacy/provider.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/localstack-core/localstack/services/apigateway/legacy/provider.py b/localstack-core/localstack/services/apigateway/legacy/provider.py index dfb27be992d02..d2a0ab459c7d6 100644 --- a/localstack-core/localstack/services/apigateway/legacy/provider.py +++ b/localstack-core/localstack/services/apigateway/legacy/provider.py @@ -1938,6 +1938,8 @@ def put_integration( ) elif integration_type in (IntegrationType.AWS_PROXY, IntegrationType.AWS): + if not request.get("integrationHttpMethod"): + raise BadRequestException("Enumeration value for HttpMethod must be non-empty") if not (integration_uri := request.get("uri") or "").startswith("arn:"): raise BadRequestException("Invalid ARN specified in the request") @@ -1961,15 +1963,6 @@ def put_integration( "Lambda function and Firehose stream invocations." ) - moto_rest_api = get_moto_rest_api(context=context, rest_api_id=request.get("restApiId")) - resource = moto_rest_api.resources.get(request.get("resourceId")) - if not resource: - raise NotFoundException("Invalid Resource identifier specified") - - method = resource.resource_methods.get(request.get("httpMethod")) - if not method: - raise NotFoundException("Invalid Method identifier specified") - # TODO: if the IntegrationType is AWS, `credentials` is mandatory moto_request = copy.copy(request) moto_request.setdefault("passthroughBehavior", "WHEN_NO_MATCH") From db7a33080e782784859a8758984c1a6115716bbd Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Mon, 3 Mar 2025 15:32:12 +0100 Subject: [PATCH 4/4] revert bad change --- .../localstack/services/apigateway/legacy/provider.py | 9 +++++++++ .../apigateway/test_apigateway_api.snapshot.json | 2 +- .../apigateway/test_apigateway_api.validation.json | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/localstack-core/localstack/services/apigateway/legacy/provider.py b/localstack-core/localstack/services/apigateway/legacy/provider.py index d2a0ab459c7d6..8eb15c4f31dd2 100644 --- a/localstack-core/localstack/services/apigateway/legacy/provider.py +++ b/localstack-core/localstack/services/apigateway/legacy/provider.py @@ -1963,6 +1963,15 @@ def put_integration( "Lambda function and Firehose stream invocations." ) + moto_rest_api = get_moto_rest_api(context=context, rest_api_id=request.get("restApiId")) + resource = moto_rest_api.resources.get(request.get("resourceId")) + if not resource: + raise NotFoundException("Invalid Resource identifier specified") + + method = resource.resource_methods.get(request.get("httpMethod")) + if not method: + raise NotFoundException("Invalid Method identifier specified") + # TODO: if the IntegrationType is AWS, `credentials` is mandatory moto_request = copy.copy(request) moto_request.setdefault("passthroughBehavior", "WHEN_NO_MATCH") diff --git a/tests/aws/services/apigateway/test_apigateway_api.snapshot.json b/tests/aws/services/apigateway/test_apigateway_api.snapshot.json index 7ee4fe320b17c..bc468b917f7cd 100644 --- a/tests/aws/services/apigateway/test_apigateway_api.snapshot.json +++ b/tests/aws/services/apigateway/test_apigateway_api.snapshot.json @@ -3509,7 +3509,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayIntegration::test_put_integration_response_validation": { - "recorded-date": "21-08-2024, 15:09:28", + "recorded-date": "03-03-2025, 14:27:24", "recorded-content": { "put-integration-wrong-method": { "Error": { diff --git a/tests/aws/services/apigateway/test_apigateway_api.validation.json b/tests/aws/services/apigateway/test_apigateway_api.validation.json index 26d28a4fb9f17..f3bba818d4d8d 100644 --- a/tests/aws/services/apigateway/test_apigateway_api.validation.json +++ b/tests/aws/services/apigateway/test_apigateway_api.validation.json @@ -132,7 +132,7 @@ "last_validated_date": "2024-12-12T10:46:41+00:00" }, "tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayIntegration::test_put_integration_response_validation": { - "last_validated_date": "2024-08-21T15:09:28+00:00" + "last_validated_date": "2025-03-03T14:27:24+00:00" }, "tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayIntegration::test_put_integration_wrong_type": { "last_validated_date": "2024-04-15T20:48:47+00:00"