8
8
import string
9
9
from typing import TYPE_CHECKING , Any , Optional , Tuple
10
10
11
- from localstack .aws .api import RequestContext
11
+ from localstack .aws .api import CommonServiceException , RequestContext
12
12
from localstack .aws .api import lambda_ as api_spec
13
13
from localstack .aws .api .lambda_ import (
14
14
AliasConfiguration ,
27
27
TracingConfig ,
28
28
VpcConfigResponse ,
29
29
)
30
+ from localstack .services .lambda_ .invocation import AccessDeniedException
30
31
from localstack .services .lambda_ .runtimes import ALL_RUNTIMES , VALID_LAYER_RUNTIMES , VALID_RUNTIMES
31
32
from localstack .utils .aws .arns import ARN_PARTITION_REGEX , get_partition
32
33
from localstack .utils .collections import merge_recursive
48
49
rf"{ ARN_PARTITION_REGEX } :lambda:(?P<region_name>[^:]+):(?P<account_id>\d{{12}}):function:(?P<function_name>[^:]+)(:(?P<qualifier>.*))?$"
49
50
)
50
51
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
52
53
LAYER_VERSION_ARN_PATTERN = re .compile (
53
54
rf"{ ARN_PARTITION_REGEX } :lambda:(?P<region_name>[^:]+):(?P<account_id>\d{{12}}):layer:(?P<layer_name>[^:]+)(:(?P<layer_version>\d+))?$"
54
55
)
102
103
# An unordered list of all Lambda CPU architectures supported by LocalStack.
103
104
ARCHITECTURES = [Architecture .arm64 , Architecture .x86_64 ]
104
105
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
+
105
165
106
166
def map_function_url_config (model : "FunctionUrlConfig" ) -> api_spec .FunctionUrlConfig :
107
167
return api_spec .FunctionUrlConfig (
@@ -185,14 +245,22 @@ def get_function_name(function_arn_or_name: str, context: RequestContext) -> str
185
245
return name
186
246
187
247
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 ]:
189
249
"""
190
250
Takes a full or partial arn, or a name
191
251
192
252
:param arn: Given arn (or name)
193
253
:return: tuple with (name, qualifier, account, region). Qualifier and region are none if missing
194
254
"""
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
196
264
197
265
198
266
def get_account_and_region (function_arn_or_name : str , context : RequestContext ) -> Tuple [str , str ]:
@@ -210,25 +278,57 @@ def get_name_and_qualifier(
210
278
function_arn_or_name : str , qualifier : str | None , context : RequestContext
211
279
) -> tuple [str , str | None ]:
212
280
"""
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.
215
282
216
283
:param function_arn_or_name: Given arn (or name)
217
284
:param qualifier: A qualifier for the function (or None)
218
285
:param context: Request context
219
286
: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)
220
291
"""
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
+
222
326
if qualifier and arn_qualifier and arn_qualifier != qualifier :
223
327
raise InvalidParameterValueException (
224
328
"The derived qualifier from the function name does not match the specified qualifier." ,
225
329
Type = "User" ,
226
330
)
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
+
232
332
qualifier = qualifier or arn_qualifier
233
333
return function_name , qualifier
234
334
@@ -627,5 +727,35 @@ def is_layer_arn(layer_name: str) -> bool:
627
727
return LAYER_VERSION_ARN_PATTERN .match (layer_name ) is not None
628
728
629
729
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
+ }
0 commit comments