Skip to content

Commit 554bcb3

Browse files
committed
fix: throw ValidationException instead of InvalidRequestContent when function name is invalid
1 parent 987223c commit 554bcb3

File tree

6 files changed

+124
-212
lines changed

6 files changed

+124
-212
lines changed

samcli/lib/utils/name_utils.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@
99
# Constants for ARN parsing
1010
PARTIAL_ARN_MIN_PARTS = 3
1111
FUNCTION_ALIAS_PARTS = 2
12+
MIN_ARN_PARTS = 5 # Minimum parts for a valid ARN structure
1213

13-
# AWS Lambda function name validation pattern (same as AWS Lambda service)
1414
LAMBDA_FUNCTION_NAME_PATTERN = (
15-
r"^(arn:(aws[a-zA-Z-]*)?:lambda:)?((eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\d{1}:)?"
16-
r"(\d{12}:)?(function:)?([a-zA-Z0-9-_\.]+)(:(\$LATEST(\.PUBLISHED)?|[a-zA-Z0-9-_]+))?$"
15+
r"^(arn:[^:]+:lambda:[^:]*:\d{12}:function:|\d{12}:function:)?[a-zA-Z0-9-_\.]+(:[\w$-]+)?$"
1716
)
1817

1918

@@ -65,13 +64,6 @@ def normalize_lambda_function_name(function_identifier: str) -> str:
6564
>>> normalize_lambda_function_name("123456789012:function:my-function")
6665
"my-function"
6766
"""
68-
if not re.match(LAMBDA_FUNCTION_NAME_PATTERN, function_identifier):
69-
raise InvalidFunctionNameException(
70-
f"1 validation error detected: Value '{function_identifier}' at 'functionName' "
71-
f"failed to satisfy constraint: Member must satisfy regular expression pattern: "
72-
f"{LAMBDA_FUNCTION_NAME_PATTERN}"
73-
)
74-
7567
# Handle full ARN format: arn:aws:lambda:region:account-id:function:function-name[:version]
7668
if function_identifier.startswith("arn:"):
7769
try:
@@ -82,9 +74,23 @@ def normalize_lambda_function_name(function_identifier: str) -> str:
8274
# Handle versioned functions by splitting on ':'
8375
function_name = arn_parts.resource_id.split(":")[0] if arn_parts.resource_id else ""
8476
return function_name if function_name else function_identifier
77+
elif arn_parts.service != "lambda":
78+
# Non-Lambda ARNs should raise an exception
79+
raise InvalidFunctionNameException(
80+
f"1 validation error detected: Value '{function_identifier}' at 'functionName' "
81+
f"failed to satisfy constraint: Member must satisfy regular expression pattern: "
82+
f"{LAMBDA_FUNCTION_NAME_PATTERN}"
83+
)
8584
except InvalidArnValue:
86-
# If ARN parsing fails, return the original input
87-
pass
85+
# Very malformed ARNs (like "arn:aws:lambda") should raise an exception
86+
if len(function_identifier.split(":")) < MIN_ARN_PARTS: # ARNs with less than 5 parts are too malformed
87+
raise InvalidFunctionNameException(
88+
f"1 validation error detected: Value '{function_identifier}' at 'functionName' "
89+
f"failed to satisfy constraint: Member must satisfy regular expression pattern: "
90+
f"{LAMBDA_FUNCTION_NAME_PATTERN}"
91+
)
92+
# Other malformed ARNs are returned unchanged
93+
return function_identifier
8894

8995
# Handle partial ARN format: account-id:function:function-name[:version]
9096
# This format has at least 3 parts separated by colons
@@ -95,15 +101,33 @@ def normalize_lambda_function_name(function_identifier: str) -> str:
95101
# Extract function name (3rd part) and remove any version suffix
96102
function_name = parts[2]
97103
return function_name if function_name else function_identifier
104+
# Invalid partial ARNs (like empty function name) should raise exception if they look like partial ARNs
105+
elif len(parts) >= PARTIAL_ARN_MIN_PARTS and parts[1] == "function":
106+
# This looks like a partial ARN but has invalid structure
107+
raise InvalidFunctionNameException(
108+
f"1 validation error detected: Value '{function_identifier}' at 'functionName' "
109+
f"failed to satisfy constraint: Member must satisfy regular expression pattern: "
110+
f"{LAMBDA_FUNCTION_NAME_PATTERN}"
111+
)
112+
# Other invalid partial ARNs are returned unchanged
113+
return function_identifier
98114

99115
# Handle function name with alias: my-function:alias
100116
# This is a simple function name with a single colon for alias/version
101117
# But exclude partial ARN patterns like "account:function" (missing function name)
102118
elif ":" in function_identifier:
103119
parts = function_identifier.split(":")
104-
if len(parts) == FUNCTION_ALIAS_PARTS and parts[1] != "function" and parts[0]:
105-
# Return just the function name part (before the colon) if it's not empty
120+
if len(parts) == FUNCTION_ALIAS_PARTS and parts[1] != "function" and parts[0] and parts[1]:
121+
# Return just the function name part (before the colon) if both parts are not empty
106122
return parts[0]
107123

124+
# Validate plain function names against the pattern
125+
if not re.match(LAMBDA_FUNCTION_NAME_PATTERN, function_identifier):
126+
raise InvalidFunctionNameException(
127+
f"1 validation error detected: Value '{function_identifier}' at 'functionName' "
128+
f"failed to satisfy constraint: Member must satisfy regular expression pattern: "
129+
f"{LAMBDA_FUNCTION_NAME_PATTERN}"
130+
)
131+
108132
# Handle plain function name: my-function
109133
return function_identifier

samcli/local/lambda_service/lambda_error_responses.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class LambdaErrorResponses:
1919
# The request body could not be parsed as JSON.
2020
InvalidRequestContentException = ("InvalidRequestContent", 400)
2121

22+
# One or more parameters values were invalid.
23+
ValidationException = ("ValidationException", 400)
24+
2225
NotImplementedException = ("NotImplemented", 501)
2326

2427
ContainerCreationFailed = ("ContainerCreationFailed", 501)
@@ -85,6 +88,29 @@ def invalid_request_content(message):
8588
exception_tuple[1],
8689
)
8790

91+
@staticmethod
92+
def validation_exception(message):
93+
"""
94+
Creates a Lambda Service ValidationException Response
95+
96+
Parameters
97+
----------
98+
message str
99+
Message to be added to the body of the response
100+
101+
Returns
102+
-------
103+
Flask.Response
104+
A response object representing the ValidationException Error
105+
"""
106+
exception_tuple = LambdaErrorResponses.ValidationException
107+
108+
return BaseLocalService.service_response(
109+
LambdaErrorResponses._construct_error_response_body(LambdaErrorResponses.USER_ERROR, message),
110+
LambdaErrorResponses._construct_headers(exception_tuple[0]),
111+
exception_tuple[1],
112+
)
113+
88114
@staticmethod
89115
def unsupported_media_type(content_type):
90116
"""

samcli/local/lambda_service/local_lambda_invoke_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def _invoke_request_handler(self, function_name):
172172
normalized_function_name = normalize_lambda_function_name(function_name)
173173
except InvalidFunctionNameException as e:
174174
LOG.error("Invalid function name: %s", str(e))
175-
return LambdaErrorResponses.invalid_request_content(str(e))
175+
return LambdaErrorResponses.validation_exception(str(e))
176176

177177
stdout_stream_string = io.StringIO()
178178
stdout_stream_bytes = io.BytesIO()

tests/unit/lib/utils/test_name_utils.py

Lines changed: 47 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -2,161 +2,59 @@
22
Tests for samcli.lib.utils.name_utils module
33
"""
44

5-
import pytest
5+
from unittest import TestCase
6+
7+
from parameterized import parameterized
68

79
from samcli.lib.utils.name_utils import normalize_lambda_function_name, InvalidFunctionNameException
810

911

10-
class TestNormalizeLambdaFunctionName:
12+
class TestNormalizeLambdaFunctionName(TestCase):
1113
"""Test cases for normalize_lambda_function_name function"""
1214

13-
def test_returns_function_name_unchanged_when_not_arn(self):
14-
"""Test that regular function names are returned unchanged"""
15-
function_name = "my-function"
16-
result = normalize_lambda_function_name(function_name)
17-
assert result == function_name
18-
19-
def test_extracts_function_name_from_basic_lambda_arn(self):
20-
"""Test extracting function name from basic Lambda ARN"""
21-
arn = "arn:aws:lambda:us-east-1:123456789012:function:my-function"
22-
result = normalize_lambda_function_name(arn)
23-
assert result == "my-function"
24-
25-
def test_extracts_function_name_from_versioned_lambda_arn(self):
26-
"""Test extracting function name from versioned Lambda ARN"""
27-
arn = "arn:aws:lambda:us-east-1:123456789012:function:my-function:$LATEST"
28-
result = normalize_lambda_function_name(arn)
29-
assert result == "my-function"
30-
31-
def test_extracts_function_name_from_numbered_version_lambda_arn(self):
32-
"""Test extracting function name from numbered version Lambda ARN"""
33-
arn = "arn:aws:lambda:us-east-1:123456789012:function:my-function:1"
34-
result = normalize_lambda_function_name(arn)
35-
assert result == "my-function"
36-
37-
def test_extracts_function_name_from_different_partition(self):
38-
"""Test extracting function name from Lambda ARN in different partition"""
39-
arn = "arn:aws-cn:lambda:cn-north-1:123456789012:function:my-function"
40-
result = normalize_lambda_function_name(arn)
41-
assert result == "my-function"
42-
43-
def test_extracts_function_name_from_partial_arn(self):
44-
"""Test extracting function name from partial ARN format"""
45-
partial_arn = "123456789012:function:my-function"
46-
result = normalize_lambda_function_name(partial_arn)
47-
assert result == "my-function"
48-
49-
def test_extracts_function_name_from_partial_arn_with_version(self):
50-
"""Test extracting function name from partial ARN with version"""
51-
partial_arn = "123456789012:function:my-function:$LATEST"
52-
result = normalize_lambda_function_name(partial_arn)
53-
assert result == "my-function"
54-
55-
def test_extracts_function_name_from_partial_arn_with_numeric_version(self):
56-
"""Test extracting function name from partial ARN with numeric version"""
57-
partial_arn = "123456789012:function:my-function:1"
58-
result = normalize_lambda_function_name(partial_arn)
59-
assert result == "my-function"
60-
61-
def test_extracts_function_name_with_alias(self):
62-
"""Test extracting function name from function name with alias"""
63-
function_with_alias = "my-function:v1"
64-
result = normalize_lambda_function_name(function_with_alias)
65-
assert result == "my-function"
66-
67-
def test_extracts_function_name_with_latest_alias(self):
68-
"""Test extracting function name from function name with $LATEST alias"""
69-
function_with_alias = "my-function:$LATEST"
70-
result = normalize_lambda_function_name(function_with_alias)
71-
assert result == "my-function"
72-
73-
def test_raises_exception_for_non_lambda_arn(self):
74-
"""Test that non-Lambda ARNs raise InvalidFunctionNameException"""
75-
arn = "arn:aws:s3:::my-bucket/my-key"
76-
with pytest.raises(InvalidFunctionNameException):
77-
normalize_lambda_function_name(arn)
78-
79-
def test_returns_unchanged_for_invalid_arn(self):
80-
"""Test that invalid ARNs are returned unchanged"""
81-
invalid_arn = "not-an-arn"
82-
result = normalize_lambda_function_name(invalid_arn)
83-
assert result == invalid_arn
84-
85-
def test_raises_exception_for_malformed_arn(self):
86-
"""Test that malformed ARNs raise InvalidFunctionNameException"""
87-
malformed_arn = "arn:aws:lambda"
88-
with pytest.raises(InvalidFunctionNameException):
89-
normalize_lambda_function_name(malformed_arn)
90-
91-
def test_returns_unchanged_for_invalid_partial_arn(self):
92-
"""Test that invalid partial ARNs are returned unchanged"""
93-
invalid_partial = "123456789012:invalid:my-function"
94-
result = normalize_lambda_function_name(invalid_partial)
95-
assert result == invalid_partial
96-
97-
def test_handles_function_name_with_hyphens_and_underscores(self):
98-
"""Test handling function names with special characters"""
99-
function_name = "my-function_name"
100-
result = normalize_lambda_function_name(function_name)
101-
assert result == function_name
102-
103-
def test_extracts_function_name_with_special_characters_from_arn(self):
104-
"""Test extracting function name with special characters from ARN"""
105-
arn = "arn:aws:lambda:us-east-1:123456789012:function:my-function_name-test"
106-
result = normalize_lambda_function_name(arn)
107-
assert result == "my-function_name-test"
108-
109-
def test_extracts_function_name_with_special_characters_from_partial_arn(self):
110-
"""Test extracting function name with special characters from partial ARN"""
111-
partial_arn = "123456789012:function:my-function_name-test"
112-
result = normalize_lambda_function_name(partial_arn)
113-
assert result == "my-function_name-test"
114-
115-
def test_handles_complex_function_names(self):
116-
"""Test handling complex function names with multiple special characters"""
117-
complex_names = [
118-
"my-function-with-many-hyphens",
119-
"my_function_with_underscores",
120-
"MyFunctionWithCamelCase",
121-
"function123WithNumbers",
122-
"function-with_mixed-chars123",
15+
@parameterized.expand(
16+
[
17+
# Simple function names (pass-through)
18+
("my-function", "my-function"),
19+
("function-with_mixed-chars123", "function-with_mixed-chars123"),
20+
# Full ARNs with versions/aliases
21+
("arn:aws:lambda:us-east-1:123456789012:function:my-function", "my-function"),
22+
("arn:aws:lambda:us-east-1:123456789012:function:my-function:$LATEST", "my-function"),
23+
("arn:aws:lambda:us-east-1:123456789012:function:my-function:1", "my-function"),
24+
("arn:aws-cn:lambda:cn-north-1:123456789012:function:my-function", "my-function"),
25+
# Partial ARNs
26+
("123456789012:function:my-function", "my-function"),
27+
("123456789012:function:my-function:$LATEST", "my-function"),
28+
# Function names with versions/aliases
29+
("my-function:$LATEST", "my-function"),
30+
("my-function:v1", "my-function"),
31+
# Invalid partial ARN format (should pass through)
32+
("123456789012:invalid:my-function", "123456789012:invalid:my-function"),
12333
]
124-
125-
for name in complex_names:
126-
# Test plain function name
127-
result = normalize_lambda_function_name(name)
128-
assert result == name
129-
130-
# Test with alias
131-
result = normalize_lambda_function_name(f"{name}:v1")
132-
assert result == name
133-
134-
# Test with full ARN
135-
arn = f"arn:aws:lambda:us-east-1:123456789012:function:{name}"
136-
result = normalize_lambda_function_name(arn)
137-
assert result == name
138-
139-
# Test with partial ARN
140-
partial_arn = f"123456789012:function:{name}"
141-
result = normalize_lambda_function_name(partial_arn)
142-
assert result == name
143-
144-
def test_raises_exception_for_invalid_function_names(self):
145-
"""Test that invalid function names raise InvalidFunctionNameException"""
146-
invalid_names = [
147-
":HelloWorld", # Starts with colon
148-
"HelloWorld:", # Ends with colon (empty alias)
149-
"", # Empty string
150-
":", # Just colon
151-
"function-with-invalid-chars!", # Invalid characters
152-
"123456789012:function:", # Partial ARN with empty function name
34+
)
35+
def test_normalize_lambda_function_name(self, input_name, expected_output):
36+
"""Test normalize_lambda_function_name with various inputs"""
37+
result = normalize_lambda_function_name(input_name)
38+
self.assertEqual(result, expected_output)
39+
40+
@parameterized.expand(
41+
[
42+
("arn:aws:s3:::my-bucket/my-key",),
43+
("arn:aws:lambda",),
44+
(":HelloWorld",),
45+
("HelloWorld:",),
46+
("",),
47+
(":",),
48+
("function-with-invalid-chars!",),
49+
("123456789012:function:",),
15350
]
51+
)
52+
def test_raises_exception_for_invalid_function_names(self, invalid_name):
53+
"""Test that invalid function names raise InvalidFunctionNameException"""
54+
with self.assertRaises(InvalidFunctionNameException) as exc_info:
55+
normalize_lambda_function_name(invalid_name)
15456

155-
for invalid_name in invalid_names:
156-
with pytest.raises(InvalidFunctionNameException) as exc_info:
157-
normalize_lambda_function_name(invalid_name)
158-
159-
# Check that the error message matches AWS Lambda's format
160-
assert "1 validation error detected" in str(exc_info.value)
161-
assert f"Value '{invalid_name}' at 'functionName'" in str(exc_info.value)
162-
assert "failed to satisfy constraint" in str(exc_info.value)
57+
# Check that the error message matches AWS Lambda's format
58+
self.assertIn("1 validation error detected", str(exc_info.exception))
59+
self.assertIn(f"Value '{invalid_name}' at 'functionName'", str(exc_info.exception))
60+
self.assertIn("failed to satisfy constraint", str(exc_info.exception))

tests/unit/local/lambda_service/test_lambda_error_responses.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,18 @@ def test_invalid_request_content(self, service_response_mock):
2626
response = LambdaErrorResponses.invalid_request_content("InvalidRequestContent")
2727

2828
self.assertEqual(response, "InvalidRequestContent")
29+
30+
@patch("samcli.local.services.base_local_service.BaseLocalService.service_response")
31+
def test_validation_exception(self, service_response_mock):
32+
service_response_mock.return_value = "ValidationException"
33+
34+
response = LambdaErrorResponses.validation_exception("ValidationException")
35+
36+
self.assertEqual(response, "ValidationException")
37+
2938
service_response_mock.assert_called_once_with(
30-
'{"Type": "User", "Message": "InvalidRequestContent"}',
31-
{"x-amzn-errortype": "InvalidRequestContent", "Content-Type": "application/json"},
39+
'{"Type": "User", "Message": "ValidationException"}',
40+
{"x-amzn-errortype": "ValidationException", "Content-Type": "application/json"},
3241
400,
3342
)
3443

0 commit comments

Comments
 (0)