8000 Lambda: improve function name and qualifier validation (#11366) · localstack/localstack@ff1c31b · GitHub
[go: up one dir, main page]

Skip to content

Commit ff1c31b

Browse files
authored
Lambda: improve function name and qualifier validation (#11366)
1 parent 0f99af7 commit ff1c31b

File tree

5 files changed

+1549
-63
lines changed

5 files changed

+1549
-63
lines changed

localstack-core/localstack/services/lambda_/api_utils.py

Lines changed: 144 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import string
99
from typing import TYPE_CHECKING, Any, Optional, Tuple
1010

11-
from localstack.aws.api import RequestContext
11+
from localstack.aws.api import CommonServiceException, RequestContext
1212
from localstack.aws.api import lambda_ as api_spec
1313
from localstack.aws.api.lambda_ import (
1414
AliasConfiguration,
@@ -27,6 +27,7 @@
2727
TracingConfig,
2828
VpcConfigResponse,
2929
)
30+
from localstack.services.lambda_.invocation import AccessDeniedException
3031
from localstack.services.lambda_.runtimes import ALL_RUNTIMES, VALID_LAYER_RUNTIMES, VALID_RUNTIMES
3132
from localstack.utils.aws.arns import ARN_PARTITION_REGEX, get_partition
3233
from localstack.utils.collections import merge_recursive
@@ -48,7 +49,7 @@
4849
rf"{ARN_PARTITION_REGEX}:lambda:(?P<region_name>[^:]+):(?P<account_id>\d{{12}}):function:(?P<function_name>[^:]+)(:(?P<qualifier>.*))?$"
4950
)
5051

51-
# Pattern for a full (both with and without qualifier) lambda function ARN
52+
# Pattern for a full (both with and without qualifier) lambda layer ARN
5253
LAYER_VERSION_ARN_PATTERN = re.compile(
5354
rf"{ARN_PARTITION_REGEX}:lambda:(?P<region_name>[^:]+):(?P<account_id>\d{{12}}):layer:(?P<layer_name>[^:]+)(:(?P<layer_version>\d+))?$"
5455
)
@@ -102,6 +103,65 @@
102103
# An unordered list of all Lambda CPU architectures supported by LocalStack.
103104
ARCHITECTURES = [Architecture.arm64, Architecture.x86_64]
104105

106+
# ARN pattern returned in validation exception messages.
107+
# Some excpetions from AWS return a '\.' in the function name regex
108+
# pattern therefore we can sub this value in when appropriate.
109+
ARN_NAME_PATTERN_VALIDATION_TEMPLATE = "(arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{{2}}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{{1}}:)?(\\d{{12}}:)?(function:)?([a-zA-Z0-9-_{0}]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?"
110+
111+
112+
def validate_function_name(function_name_or_arn: str, operation_type: str):
113+
function_name, *_ = function_locators_from_arn(function_name_or_arn)
114+
arn_name_pattern = ARN_NAME_PATTERN_VALIDATION_TEMPLATE.format("")
115+
max_length = 170
116+
117+
match operation_type:
118+
case "GetFunction" | "Invoke":
119+
arn_name_pattern = ARN_NAME_PATTERN_VALIDATION_TEMPLATE.format(r"\.")
120+
case "CreateFunction" if function_name == function_name_or_arn: # only a function name
121+
max_length = 64
122+
case "CreateFunction" | "DeleteFunction":
123+
max_length = 140
124+
125+
validations = []
126+
if len(function_name_or_arn) > max_length:
127+
constraint = f"Member must have length less than or equal to {max_length}"
128+
validation_msg = f"Value '{function_name_or_arn}' at 'functionName' failed to satisfy constraint: {constraint}"
129+
validations.append(validation_msg)
130+
131+
if not AWS_FUNCTION_NAME_REGEX.match(function_name_or_arn) or not function_name:
132+
constraint = f"Member must satisfy regular expression pattern: {arn_name_pattern}"
133+
validation_msg = f"Value '{function_name_or_arn}' at 'functionName' failed to satisfy constraint: {constraint}"
134+
validations.append(validation_msg)
135+
136+
return validations
137+
138+
139+
def validate_qualifier(qualifier: str):
140+
validations = []
141+
142+
if len(qualifier) > 128:
143+
constraint = "Member must have length less than or equal to 128"
144+
validation_msg = (
145+
f"Value '{qualifier}' at 'qualifier' failed to satisfy constraint: {constraint}"
146+
)
147+
validations.append(validation_msg)
148+
149+
if not QUALIFIER_REGEX.match(qualifier):
150+
constraint = "Member must satisfy regular expression pattern: (|[a-zA-Z0-9$_-]+)"
151+
validation_msg = (
152+
f"Value '{qualifier}' at 'qualifier' failed to satisfy constraint: {constraint}"
153+
)
154+
validations.append(validation_msg)
155+
156+
return validations
157+
158+
159+
def construct_validation_exception_message(validation_errors):
160+
if validation_errors:
161+
return f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: {'; '.join(validation_errors)}"
162+
163+
return None
164+
105165

106166
def map_function_url_config(model: "FunctionUrlConfig") -> api_spec.FunctionUrlConfig:
107167
return api_spec.FunctionUrlConfig(
@@ -185,14 +245,22 @@ def get_function_name(function_arn_or_name: str, context: RequestContext) -> str
185245
return name
186246

187247

188-
def function_locators_from_arn(arn: str) -> tuple[str, str | None, str | None, str | None]:
248+
def function_locators_from_arn(arn: str) -> tuple[str | None, str | None, str | None, str | None]:
189249
"""
190250
Takes a full or partial arn, or a name
191251
192252
:param arn: Given arn (or name)
193253
:return: tuple with (name, qualifier, account, region). Qualifier and region are none if missing
194254
"""
195-
return FUNCTION_NAME_REGEX.match(arn).group("name", "qualifier", "account", "region")
255+
256+
if matched := FUNCTION_NAME_REGEX.match(arn):
257+
name = matched.group("name")
258+
qualifier = matched.group("qualifier")
259+
account = matched.group("account")
260+
region = matched.group("region")
261+
return (name, qualifier, account, region)
262+
263+
return None, None, None, None
196264

197265

198266
def get_account_and_region(function_arn_or_name: str, context: RequestContext) -> Tuple[str, str]:
@@ -210,25 +278,57 @@ def get_name_and_qualifier(
210278
function_arn_or_name: str, qualifier: str | None, context: RequestContext
211279
) -> tuple[str, str | None]:
212280
"""
213-
Takes a full or partial arn, or a name and a qualifier
214-
Will raise exception if a qualified arn is provided and the qualifier does not match (but is given)
281+
Takes a full or partial arn, or a name and a qualifier.
215282
216283
:param function_arn_or_name: Given arn (or name)
217284
:param qualifier: A qualifier for the function (or None)
218285
:param context: Request context
219286
:return: tuple with (name, qualifier). Qualifier is none if missing
287+
:raises: `ResourceNotFoundException` when the context's region differs from the ARN's region
288+
:raises: `AccessDeniedException` when the context's account ID differs from the ARN's account ID
289+
:raises: `ValidationExcpetion` when a function ARN/name or qualifier fails validation checks
290+
:raises: `InvalidParameterValueException` when a qualified arn is provided and the qualifier does not match (but is given)
220291
"""
221-
function_name, arn_qualifier, _, arn_region = function_locators_from_arn(function_arn_or_name)
292+
function_name, arn_qualifier, account, region = function_locators_from_arn(function_arn_or_name)
293+
operation_type = context.operation.name
294+
295+
if operation_type not in _supported_resource_based_operations:
296+
if account and account != context.account_id:
297+
raise AccessDeniedException(None)
298+
299+
# TODO: should this only run if operation type is unsupported?
300+
if region and region != context.region:
301+
raise ResourceNotFoundException(
302+
f"Functions from '{region}' are not reachable in this region ('{context.region}')",
303+
Type="User",
304+
)
305+
306+
validation_errors = []
307+
if function_arn_or_name:
308+
validation_errors.extend(validate_function_name(function_arn_or_name, operation_type))
309+
310+
if qualifier:
311+
validation_errors.extend(validate_qualifier(qualifier))
312+
313+
is_only_function_name = function_arn_or_name == function_name
314+
if validation_errors:
315+
message = construct_validation_exception_message(validation_errors)
316+
# Edge-case where the error type is not ValidationException
317+
if (
318+
operation_type == "CreateFunction"
319+
and is_only_function_name
320+
and arn_qualifier is None
321+
and region is None
322+
): # just name OR partial
323+
raise InvalidParameterValueException(message=message, Type="User")
324+
raise CommonServiceException(message=message, code="ValidationException")
325+
222326
if qualifier and arn_qualifier and arn_qualifier != qualifier:
223327
raise InvalidParameterValueException(
224328
"The derived qualifier from the function name does not match the specified qualifier.",
225329
Type="User",
226330
)
227-
if arn_region and arn_region != context.region:
228-
raise ResourceNotFoundException(
229-
f"Functions from '{arn_region}' are not reachable in this region ('{context.region}')",
230-
Type="User",
231-
)
331+
232332
qualifier = qualifier or arn_qualifier
233333
return function_name, qualifier
234334

@@ -627,5 +727,35 @@ def is_layer_arn(layer_name: str) -> bool:
627727
return LAYER_VERSION_ARN_PATTERN.match(layer_name) is not None
628728

629729

630-
def validate_function_name(function_name):
631-
return AWS_FUNCTION_NAME_REGEX.match(function_name)
730+
# See Lambda API actions that support resource-based IAM policies
731+
# https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html#permissions-resource-api
732+
_supported_resource_based_operations = {
733+
"CreateAlias",
734+
"DeleteAlias",
735+
"DeleteFunction",
736+
"DeleteFunctionConcurrency",
737+
"DeleteFunctionEventInvokeConfig",
738+
"DeleteProvisionedConcurrencyConfig",
739+
"GetAlias",
740+
"GetFunction",
741+
"GetFunctionConcurrency",
742+
"GetFunctionConfiguration",
743+
"GetFunctionEventInvokeConfig",
744+
"GetPolicy",
745+
"GetProvisionedConcurrencyConfig",
746+
"Invoke",
747+
"ListAliases",
748+
"ListFunctionEventInvokeConfigs",
749+
"ListProvisionedConcurrencyConfigs",
750+
"ListTags",
751+
"ListVersionsByFunction",
752+
"PublishVersion",
753+
"PutFunctionConcurrency",
754+
"PutFunctionEventInvokeConfig",
755+
"PutProvisionedConcurrencyConfig",
756+
"TagResource",
757+
"UntagResource",
758+
"UpdateAlias",
759+
"UpdateFunctionCode",
760+
"UpdateFunctionEventInvokeConfig",
761+
}

localstack-core/localstack/services/lambda_/provider.py

Lines changed: 16 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -356,10 +356,9 @@ def _get_function(function_name: str, account_id: str, region: str) -> Function:
356356

357357
@staticmethod
358358
def _validate_qualifier_expression(qualifier: str) -> None:
359-
if not api_utils.is_qualifier_expression(qualifier):
359+
if error_messages := api_utils.validate_qualifier(qualifier):
360360
raise ValidationException(
361-
f"1 validation error detected: Value '{qualifier}' at 'qualifier' failed to satisfy constraint: "
362-
f"Member must satisfy regular expression pattern: (|[a-zA-Z0-9$_-]+)"
361+
message=api_utils.construct_validation_exception_message(error_messages)
363362
)
364363

365364
@staticmethod
@@ -769,43 +768,13 @@ def create_function(
769768
runtime = request.get("Runtime")
770769
self._validate_runtime(package_type, runtime)
771770

772-
request_function_name = request["FunctionName"]
773-
# Validate FunctionName:
774-
# a) Function name: just function name (max 64 chars)
775-
# b) Function ARN: unqualified arn (min 1, max 64 chars)
776-
# c) Partial ARN: ACCOUNT_ID:function:FUNCTION_NAME
777-
function_name, qualifier, account, region = function_locators_from_arn(
778-
request_function_name
771+
request_function_name = request.get("FunctionName")
772+
773+
function_name, *_ = api_utils.get_name_and_qualifier(
774+
function_arn_or_name=request_function_name,
775+
qualifier=None,
776+
context=context,
779777
)
780-
if (
781-
function_name and qualifier is None and account is None and region is None
782-
): # just function name
783-
pass
784-
elif function_name and account and qualifier is None and region is None: # partial arn
785-
if account != context_account_id:
786-
raise AccessDeniedException(None)
787-
elif function_name and account and region and qualifier is None: # unqualified arn
788-
if len(request_function_name) > 140:
789-
raise ValidationException(
790-
f"1 validation error detected: Value '{request_function_name}' at 'functionName' failed to satisfy constraint: Member must have length less than or equal to 140"
791-
)
792-
if region != context.region:
793-
raise ResourceNotFoundException(
794-
f"Functions from '{region}' are not reachable in this region ('{context.region}')",
795-
Type="User",
796-
)
797-
if account != context_account_id:
798-
raise AccessDeniedException(None)
799-
else:
800-
pattern = r"(arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}((-gov)|(-iso(b?)))?-[a-z]+-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\$LATEST|[a-zA-Z0-9-_]+))?"
801-
raise ValidationException(
802-
f"1 validation error detected: Value '{request_function_name}' at 'functionName' failed to satisfy constraint: Member must satisfy regular expression pattern: {pattern}"
803-
)
804-
if len(function_name) > 64:
805-
raise InvalidParameterValueException(
806-
f"1 validation error detected: Value '{function_name}' at 'functionName' failed to satisfy constraint: Member must have length less than or equal to 64",
807-
Type="User",
808-
)
809778

810779
if runtime in DEPRECATED_RUNTIMES:
811780
LOG.warning(
@@ -1290,6 +1259,13 @@ def delete_function(
12901259
function_name, qualifier = api_utils.get_name_and_qualifier(
12911260
function_name, qualifier, context
12921261
)
1262+
1263+
if qualifier and api_utils.qualifier_is_alias(qualifier):
1264+
raise InvalidParameterValueException(
1265+
"Deletion of aliases is not currently supported.",
1266+
Type="User",
1267+
)
1268+
12931269
store = lambda_stores[account_id][region]
12941270
if qualifier == "$LATEST":
12951271
raise InvalidParameterValueException(
@@ -1371,6 +1347,7 @@ def get_function(
13711347
function_name, qualifier = api_utils.get_name_and_qualifier(
13721348
function_name, qualifier, context
13731349
)
1350+
13741351
fn = lambda_stores[account_id][region].functions.get(function_name)
13751352
if fn is None:
13761353
if qualifier is None:
@@ -1456,11 +1433,6 @@ def invoke(
14561433
**kwargs,
14571434
) -> InvocationResponse:
14581435
account_id, region = api_utils.get_account_and_region(function_name, context)
1459-
if not api_utils.validate_function_name(function_name):
1460-
raise ValidationException(
1461-
f"1 validation error detected: Value '{function_name}' at 'functionName' failed to satisfy constraint: Member must satisfy regular expression pattern: (arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{{2}}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{{1}}:)?(\\d{{12}}:)?(function:)?([a-zA-Z0-9-_\\.]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?"
1462-
)
1463-
14641436
function_name, qualifier = api_utils.get_name_and_qualifier(
14651437
function_name, qualifier, context
14661438
)

0 commit comments

Comments
 (0)
0