Skip to content

Commit ea3cea5

Browse files
whummerclaude
andcommitted
introduce invoke mode for Lambda proxy; block invocations in read_only
Lambda Invoke operations have side-effects and should not be treated as read operations. Removes them from _is_read_request and introduces a new 'invoke' config flag that explicitly allows invocations alongside read_only mode. Updates tests and README accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 976911c commit ea3cea5

File tree

4 files changed

+74
-11
lines changed

4 files changed

+74
-11
lines changed

aws-proxy/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ services:
8585
- 'Put.*'
8686
# optionally, specify that only read requests should be allowed (Get*/List*/Describe*, etc)
8787
read_only: false
88+
# optionally, allow invoke/execute operations (e.g., Lambda invocations) alongside read_only mode.
89+
# execute operations have side-effects and are deliberately excluded from read_only by default.
90+
execute: false
8891
```
8992

9093
Store the configuration above to a file named `proxy_config.yml`, then we can start up the proxy via:

aws-proxy/aws_proxy/server/aws_request_forwarder.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,12 @@ def select_proxy(self, context: RequestContext) -> Optional[ProxyInstance]:
8888

8989
# check if only read requests should be forwarded
9090
read_only = service_config.get("read_only")
91-
if read_only and not self._is_read_request(context):
92-
return
91+
if read_only:
92+
allow_invoke = service_config.get("execute")
93+
if not self._is_read_request(context) and not (
94+
allow_invoke and self._is_execute_request(context)
95+
):
96+
return
9397

9498
# check if any operation name pattern matches
9599
operation_names = ensure_list(service_config.get("operations", []))
@@ -277,12 +281,6 @@ def _is_read_request(self, context: RequestContext) -> bool:
277281
"PartiQLSelect",
278282
}:
279283
return True
280-
if context.service.service_name == "lambda" and operation_name in {
281-
"Invoke",
282-
"InvokeAsync",
283-
"InvokeWithResponseStream",
284-
}:
285-
return True
286284
if context.service.service_name == "appsync" and operation_name in {
287285
"EvaluateCode",
288286
"EvaluateMappingTemplate",
@@ -303,6 +301,21 @@ def _is_read_request(self, context: RequestContext) -> bool:
303301
# TODO: add more rules
304302
return False
305303

304+
def _is_execute_request(self, context: RequestContext) -> bool:
305+
"""
306+
Function to determine whether a request is an invoke/execute request.
307+
Invoke operations have side-effects and are not considered read operations.
308+
They can be explicitly allowed alongside read_only mode via the 'execute' config flag.
309+
"""
310+
operation_name = context.service_operation.operation
311+
if context.service.service_name == "lambda" and operation_name in {
312+
"Invoke",
313+
"InvokeAsync",
314+
"InvokeWithResponseStream",
315+
}:
316+
return True
317+
return False
318+
306319
def _extract_region_from_domain(self, context: RequestContext):
307320
"""
308321
If the request domain name contains a valid region name (e.g., "us-east-2.cognito.localhost.localstack.cloud"),

aws-proxy/aws_proxy/shared/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ class ProxyServiceConfig(TypedDict, total=False):
1111
operations: List[str]
1212
# whether only read requests should be forwarded
1313
read_only: bool
14+
# whether invoke/execute operations (e.g., Lambda invocations) should be forwarded
15+
# (only relevant when read_only is True, since execute has side-effects and is not a read operation)
16+
execute: bool
1417

1518

1619
class ProxyConfig(TypedDict, total=False):

aws-proxy/tests/proxy/test_lambda.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,52 @@ def _list_contains_function():
211211

212212
retry(_list_contains_function, retries=5, sleep=2)
213213

214-
# Invoke is classified as a read operation for proxy purposes
215-
# (it executes the function but doesn't modify it)
214+
# Invoke has side-effects and is NOT a read operation - should be blocked in read_only mode
215+
with pytest.raises(ClientError) as ctx:
216+
lambda_client.invoke(
217+
FunctionName=function_name,
218+
Payload=json.dumps({"key": "value"}),
219+
)
220+
assert ctx.value.response["Error"]["Code"] == "ResourceNotFoundException"
221+
222+
# UpdateFunctionConfiguration is a write operation - should be blocked
223+
with pytest.raises(ClientError) as ctx:
224+
lambda_client.update_function_configuration(
225+
FunctionName=function_name,
226+
Description="updated via proxy - should not work",
227+
)
228+
assert ctx.value.response["Error"]["Code"] == "ResourceNotFoundException"
229+
230+
231+
def test_lambda_invoke_mode(start_aws_proxy, cleanups, lambda_execution_role):
232+
"""Test Lambda proxy with read_only + invoke mode: reads and invocations proxied, writes blocked."""
233+
function_name = f"test-fn-inv-{short_uid()}"
234+
235+
# start proxy with read_only + invoke flags
236+
config = ProxyConfig(
237+
services={"lambda": {"resources": ".*", "read_only": True, "execute": True}}
238+
)
239+
start_aws_proxy(config)
240+
241+
# create clients
242+
region_name = "us-east-1"
243+
lambda_client = connect_to(region_name=region_name).lambda_
244+
lambda_client_aws = boto3.client("lambda", region_name=region_name)
245+
246+
# create function in real AWS (direct, not through proxy)
247+
_create_lambda_function(
248+
lambda_client_aws, function_name, lambda_execution_role, cleanups
249+
)
250+
251+
# read operations should be proxied
252+
fn_local = lambda_client.get_function(FunctionName=function_name)
253+
fn_aws = lambda_client_aws.get_function(FunctionName=function_name)
254+
assert (
255+
fn_local["Configuration"]["FunctionArn"]
256+
== fn_aws["Configuration"]["FunctionArn"]
257+
)
258+
259+
# Invoke should be proxied when execute: True is set alongside read_only
216260
response = lambda_client.invoke(
217261
FunctionName=function_name,
218262
Payload=json.dumps({"key": "value"}),
@@ -222,7 +266,7 @@ def _list_contains_function():
222266
body = json.loads(payload["body"])
223267
assert body["message"] == "hello"
224268

225-
# UpdateFunctionConfiguration is a write operation - should be blocked
269+
# UpdateFunctionConfiguration is a write operation - should still be blocked
226270
with pytest.raises(ClientError) as ctx:
227271
lambda_client.update_function_configuration(
228272
FunctionName=function_name,

0 commit comments

Comments
 (0)