Skip to content

Commit 6d477da

Browse files
authored
Merge branch 'main' into release-v1.60.1
2 parents b5751bd + 5cb5b59 commit 6d477da

File tree

42 files changed

+2207
-184
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2207
-184
lines changed

.cfnlintrc.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ ignore_templates:
5757
- tests/translator/output/**/connector_sfn_to_function.json
5858
- tests/translator/output/**/connector_sns_to_function.json
5959
- tests/translator/output/**/connector_table_to_function.json
60+
- tests/translator/output/**/documentdb_with_intrinsics.json # TODO: remove once DocumentDDB is available
6061
- tests/translator/output/**/eventbridgerule_with_dlq.json
6162
- tests/translator/output/**/function_event_conditions.json
6263
- tests/translator/output/**/function_with_alias_and_code_sha256.json
@@ -77,6 +78,8 @@ ignore_templates:
7778
- tests/translator/output/**/function_with_deployment_preference_multiple_combinations_conditions_without_passthrough.json
7879
- tests/translator/output/**/function_with_deployment_preference_passthrough_condition_with_supported_intrinsics.json
7980
- tests/translator/output/**/function_with_dlq.json
81+
- tests/translator/output/**/function_with_documentdb_with_kms.json # TODO: remove once DocumentDDB is available
82+
- tests/translator/output/**/function_with_documentdb.json # TODO: remove once DocumentDDB is available
8083
- tests/translator/output/**/function_with_event_dest.json
8184
- tests/translator/output/**/function_with_event_dest_basic.json
8285
- tests/translator/output/**/function_with_event_dest_conditional.json

samtranslator/internal/intrinsics.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Any, Dict, Optional, Union
2+
3+
from samtranslator.intrinsics.resolver import IntrinsicsResolver
4+
from samtranslator.model.exceptions import InvalidResourceException
5+
6+
7+
def resolve_string_parameter_in_resource(
8+
logical_id: str,
9+
intrinsics_resolver: IntrinsicsResolver,
10+
parameter_value: Optional[Union[str, Dict[str, Any]]],
11+
parameter_name: str,
12+
) -> Optional[Union[str, Dict[str, Any]]]:
13+
"""Try to resolve values in a resource from template parameters."""
14+
if not parameter_value:
15+
return parameter_value
16+
value = intrinsics_resolver.resolve_parameter_refs(parameter_value)
17+
18+
if not isinstance(value, str) and not isinstance(value, dict):
19+
raise InvalidResourceException(
20+
logical_id,
21+
"Could not resolve parameter for '{}' or parameter is not a String.".format(parameter_name),
22+
)
23+
return value

samtranslator/model/__init__.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
import inspect
33
import re
44
from abc import ABC, ABCMeta, abstractmethod
5-
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
5+
from typing import Any, Callable, Dict, List, Optional, Tuple
66

7-
from samtranslator.intrinsics.resolver import IntrinsicsResolver
87
from samtranslator.model.exceptions import ExpectedType, InvalidResourceException, InvalidResourcePropertyTypeException
98
from samtranslator.model.tags.resource_tagging import get_tag_list
109
from samtranslator.model.types import IS_DICT, IS_STR, Validator, any_type, is_type
@@ -502,23 +501,6 @@ def _check_tag(self, reserved_tag_name, tags): # type: ignore[no-untyped-def]
502501
"input.",
503502
)
504503

505-
def _resolve_string_parameter(
506-
self,
507-
intrinsics_resolver: IntrinsicsResolver,
508-
parameter_value: Optional[Union[str, Dict[str, Any]]],
509-
parameter_name: str,
510-
) -> Optional[Union[str, Dict[str, Any]]]:
511-
if not parameter_value:
512-
return parameter_value
513-
value = intrinsics_resolver.resolve_parameter_refs(parameter_value)
514-
515-
if not isinstance(value, str) and not isinstance(value, dict):
516-
raise InvalidResourceException(
517-
self.logical_id,
518-
"Could not resolve parameter for '{}' or parameter is not a String.".format(parameter_name),
519-
)
520-
return value
521-
522504

523505
class ResourceTypeResolver:
524506
"""ResourceTypeResolver maps Resource Types to Resource classes, e.g. AWS::Serverless::Function to

samtranslator/model/eventsources/pull.py

Lines changed: 170 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from abc import ABCMeta, abstractmethod
22
from typing import Any, Dict, List, Optional
33

4+
from samtranslator.internal.deprecation_control import deprecated
45
from samtranslator.metrics.method_decorator import cw_timer
56
from samtranslator.model import PassThroughProperty, PropertyType, ResourceMacro
67
from samtranslator.model.eventsources import FUNCTION_EVETSOURCE_METRIC_PREFIX
@@ -17,15 +18,15 @@
1718
class PullEventSource(ResourceMacro, metaclass=ABCMeta):
1819
"""Base class for pull event sources for SAM Functions.
1920
20-
The pull events are Kinesis Streams, DynamoDB Streams, Kafka Topics, Amazon MQ Queues and SQS Queues. All of these correspond to an
21+
The pull events are Kinesis Streams, DynamoDB Streams, Kafka Topics, Amazon MQ Queues, SQS Queues, and DocumentDB Clusters. All of these correspond to an
2122
EventSourceMapping in Lambda, and require that the execution role be given to Kinesis Streams, DynamoDB
2223
Streams, or SQS Queues, respectively.
2324
2425
:cvar str policy_arn: The ARN of the AWS managed role policy corresponding to this pull event source
2526
"""
2627

2728
# Event types that support `FilterCriteria`, stored as a list to keep the alphabetical order
28-
RESOURCE_TYPES_WITH_EVENT_FILTERING = ["DynamoDB", "Kinesis", "MQ", "MSK", "SelfManagedKafka", "SQS"]
29+
RESOURCE_TYPES_WITH_EVENT_FILTERING = ["DocumentDB", "DynamoDB", "Kinesis", "MQ", "MSK", "SelfManagedKafka", "SQS"]
2930

3031
# Note(xinhol): `PullEventSource` should have been an abstract class. Disabling the type check for the next
3132
# line to avoid any potential behavior change.
@@ -88,6 +89,14 @@ def get_policy_statements(self) -> Optional[List[Dict[str, Any]]]:
8889
def get_event_source_arn(self) -> Optional[PassThrough]:
8990
"""Return the value to assign to lambda event source mapping's EventSourceArn."""
9091

92+
def add_extra_eventsourcemapping_fields(self, _lambda_eventsourcemapping: LambdaEventSourceMapping) -> None:
93+
"""Adds extra fields to the CloudFormation ESM resource.
94+
This method can be overriden by a subclass if it has extra fields specific to that subclass.
95+
96+
:param LambdaEventSourceMapping lambda_eventsourcemapping: the Event source mapping resource to add the fields to.
97+
"""
98+
return
99+
91100
@cw_timer(prefix=FUNCTION_EVETSOURCE_METRIC_PREFIX)
92101
def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] # noqa: too-many-branches
93102
"""Returns the Lambda EventSourceMapping to which this pull event corresponds. Adds the appropriate managed
@@ -183,6 +192,8 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] # noqa: t
183192

184193
lambda_eventsourcemapping.DestinationConfig = self.DestinationConfig
185194

195+
self.add_extra_eventsourcemapping_fields(lambda_eventsourcemapping)
196+
186197
if "role" in kwargs:
187198
self._link_policy(kwargs["role"], destination_config_policy) # type: ignore[no-untyped-call]
188199

@@ -232,13 +243,71 @@ def _validate_filter_criteria(self) -> None:
232243
if list(self.FilterCriteria.keys()) not in [[], ["Filters"]]:
233244
raise InvalidEventException(self.relative_id, "FilterCriteria field has a wrong format")
234245

235-
def validate_secrets_manager_kms_key_id(self): # type: ignore[no-untyped-def]
236-
if self.SecretsManagerKmsKeyId and not isinstance(self.SecretsManagerKmsKeyId, str):
246+
def validate_secrets_manager_kms_key_id(self) -> None:
247+
if self.SecretsManagerKmsKeyId:
248+
sam_expect(
249+
self.SecretsManagerKmsKeyId, self.relative_id, "SecretsManagerKmsKeyId", is_sam_event=True
250+
).to_be_a_string()
251+
252+
def _validate_source_access_configurations(self, supported_types: List[str], required_type: str) -> str:
253+
"""
254+
Validate the SourceAccessConfigurations parameter and return the URI to
255+
be used for policy statement creation.
256+
"""
257+
258+
if not self.SourceAccessConfigurations:
237259
raise InvalidEventException(
238260
self.relative_id,
239-
"Provided SecretsManagerKmsKeyId should be of type str.",
261+
f"No SourceAccessConfigurations for Amazon {self.resource_type} event provided.",
262+
)
263+
if not isinstance(self.SourceAccessConfigurations, list):
264+
raise InvalidEventException(
265+
self.relative_id,
266+
"Provided SourceAccessConfigurations cannot be parsed into a list.",
240267
)
241268

269+
required_type_uri: Optional[str] = None
270+
for index, conf in enumerate(self.SourceAccessConfigurations):
271+
sam_expect(conf, self.relative_id, f"SourceAccessConfigurations[{index}]", is_sam_event=True).to_be_a_map()
272+
event_type: str = sam_expect(
273+
conf.get("Type"), self.relative_id, f"SourceAccessConfigurations[{index}].Type", is_sam_event=True
274+
).to_be_a_string()
275+
if event_type not in supported_types:
276+
raise InvalidEventException(
277+
self.relative_id,
278+
f"Invalid property Type specified in SourceAccessConfigurations. The supported values are: {supported_types}.",
279+
)
280+
if event_type == required_type:
281+
if required_type_uri:
282+
raise InvalidEventException(
283+
self.relative_id,
284+
f"Multiple {required_type} properties specified in SourceAccessConfigurations.",
285+
)
286+
required_type_uri = conf.get("URI")
287+
if not required_type_uri:
288+
raise InvalidEventException(
289+
self.relative_id,
290+
f"No {required_type} URI property specified in SourceAccessConfigurations.",
291+
)
292+
293+
if not required_type_uri:
294+
raise InvalidEventException(
295+
self.relative_id,
296+
f"No {required_type} property specified in SourceAccessConfigurations.",
297+
)
298+
return required_type_uri
299+
300+
@staticmethod
301+
def _get_kms_decrypt_policy(secrets_manager_kms_key_id: str) -> Dict[str, Any]:
302+
return {
303+
"Action": ["kms:Decrypt"],
304+
"Effect": "Allow",
305+
"Resource": {
306+
"Fn::Sub": "arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/"
307+
+ secrets_manager_kms_key_id
308+
},
309+
}
310+
242311

243312
class Kinesis(PullEventSource):
244313
"""Kinesis event source."""
@@ -366,45 +435,8 @@ def get_policy_arn(self) -> Optional[str]:
366435
return None
367436

368437
def get_policy_statements(self) -> Optional[List[Dict[str, Any]]]:
369-
if not self.SourceAccessConfigurations:
370-
raise InvalidEventException(
371-
self.relative_id,
372-
"No SourceAccessConfigurations for Amazon MQ event provided.",
373-
)
374-
if not isinstance(self.SourceAccessConfigurations, list):
375-
raise InvalidEventException(
376-
self.relative_id,
377-
"Provided SourceAccessConfigurations cannot be parsed into a list.",
378-
)
379-
basic_auth_uri = None
380-
for index, conf in enumerate(self.SourceAccessConfigurations):
381-
sam_expect(conf, self.relative_id, f"SourceAccessConfigurations[{index}]", is_sam_event=True).to_be_a_map()
382-
event_type: str = sam_expect(
383-
conf.get("Type"), self.relative_id, f"SourceAccessConfigurations[{index}].Type", is_sam_event=True
384-
).to_be_a_string()
385-
if event_type not in ("BASIC_AUTH", "VIRTUAL_HOST"):
386-
raise InvalidEventException(
387-
self.relative_id,
388-
"Invalid property specified in SourceAccessConfigurations for Amazon MQ event.",
389-
)
390-
if event_type == "BASIC_AUTH":
391-
if basic_auth_uri:
392-
raise InvalidEventException(
393-
self.relative_id,
394-
"Multiple BASIC_AUTH properties specified in SourceAccessConfigurations for Amazon MQ event.",
395-
)
396-
basic_auth_uri = conf.get("URI")
397-
if not basic_auth_uri:
398-
raise InvalidEventException(
399-
self.relative_id,
400-
"No BASIC_AUTH URI property specified in SourceAccessConfigurations for Amazon MQ event.",
401-
)
438+
basic_auth_uri = self._validate_source_access_configurations(["BASIC_AUTH", "VIRTUAL_HOST"], "BASIC_AUTH")
402439

403-
if not basic_auth_uri:
404-
raise InvalidEventException(
405-
self.relative_id,
406-
"No BASIC_AUTH property specified in SourceAccessConfigurations for Amazon MQ event.",
407-
)
408440
document = {
409441
"PolicyName": "SamAutoGeneratedAMQPolicy",
410442
"PolicyDocument": {
@@ -427,7 +459,7 @@ def get_policy_statements(self) -> Optional[List[Dict[str, Any]]]:
427459
},
428460
}
429461
if self.SecretsManagerKmsKeyId:
430-
self.validate_secrets_manager_kms_key_id() # type: ignore[no-untyped-call]
462+
self.validate_secrets_manager_kms_key_id()
431463
kms_policy = {
432464
"Action": "kms:Decrypt",
433465
"Effect": "Allow",
@@ -499,8 +531,8 @@ def generate_policy_document(self, source_access_configurations: List[Any]): #
499531
statements.append(vpc_permissions)
500532

501533
if self.SecretsManagerKmsKeyId:
502-
self.validate_secrets_manager_kms_key_id() # type: ignore[no-untyped-call]
503-
kms_policy = self.get_kms_policy(self.SecretsManagerKmsKeyId)
534+
self.validate_secrets_manager_kms_key_id()
535+
kms_policy = self._get_kms_decrypt_policy(self.SecretsManagerKmsKeyId)
504536
statements.append(kms_policy)
505537

506538
return {
@@ -590,6 +622,7 @@ def get_vpc_permission(self) -> Dict[str, Any]:
590622
}
591623

592624
@staticmethod
625+
@deprecated(None)
593626
def get_kms_policy(secrets_manager_kms_key_id: str) -> Dict[str, Any]:
594627
return {
595628
"Action": ["kms:Decrypt"],
@@ -599,3 +632,94 @@ def get_kms_policy(secrets_manager_kms_key_id: str) -> Dict[str, Any]:
599632
+ secrets_manager_kms_key_id
600633
},
601634
}
635+
636+
637+
class DocumentDB(PullEventSource):
638+
"""DocumentDB event source."""
639+
640+
resource_type = "DocumentDB"
641+
property_types: Dict[str, PropertyType] = {
642+
**PullEventSource.property_types,
643+
"Cluster": PassThroughProperty(True),
644+
"DatabaseName": PassThroughProperty(True),
645+
"CollectionName": PassThroughProperty(False),
646+
"FullDocument": PassThroughProperty(False),
647+
}
648+
649+
Cluster: PassThrough
650+
DatabaseName: PassThrough
651+
CollectionName: Optional[PassThrough]
652+
FullDocument: Optional[PassThrough]
653+
654+
def add_extra_eventsourcemapping_fields(self, lambda_eventsourcemapping: LambdaEventSourceMapping) -> None:
655+
lambda_eventsourcemapping.DocumentDBEventSourceConfig = {
656+
"DatabaseName": self.DatabaseName,
657+
}
658+
if self.CollectionName:
659+
lambda_eventsourcemapping.DocumentDBEventSourceConfig["CollectionName"] = self.CollectionName # type: ignore[attr-defined]
660+
if self.FullDocument:
661+
lambda_eventsourcemapping.DocumentDBEventSourceConfig["FullDocument"] = self.FullDocument # type: ignore[attr-defined]
662+
663+
def get_event_source_arn(self) -> Optional[PassThrough]:
664+
return self.Cluster
665+
666+
def get_policy_arn(self) -> Optional[str]:
667+
return None
668+
669+
def get_policy_statements(self) -> List[Dict[str, Any]]:
670+
basic_auth_uri = self._validate_source_access_configurations(["BASIC_AUTH"], "BASIC_AUTH")
671+
672+
statements = [
673+
{
674+
"Action": [
675+
"secretsmanager:GetSecretValue",
676+
],
677+
"Effect": "Allow",
678+
"Resource": basic_auth_uri,
679+
},
680+
{
681+
"Action": [
682+
"rds:DescribeDBClusterParameters",
683+
],
684+
"Effect": "Allow",
685+
"Resource": {"Fn::Sub": "arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:cluster-pg:*"},
686+
},
687+
{
688+
"Action": [
689+
"rds:DescribeDBSubnetGroups",
690+
],
691+
"Effect": "Allow",
692+
"Resource": {"Fn::Sub": "arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:subgrp:*"},
693+
},
694+
{
695+
"Action": [
696+
"rds:DescribeDBClusters",
697+
],
698+
"Effect": "Allow",
699+
"Resource": self.Cluster,
700+
},
701+
{
702+
"Action": [
703+
"ec2:CreateNetworkInterface",
704+
"ec2:DescribeNetworkInterfaces",
705+
"ec2:DeleteNetworkInterface",
706+
"ec2:DescribeVpcs",
707+
"ec2:DescribeSubnets",
708+
"ec2:DescribeSecurityGroups",
709+
],
710+
"Effect": "Allow",
711+
"Resource": "*",
712+
},
713+
]
714+
715+
if self.SecretsManagerKmsKeyId:
716+
self.validate_secrets_manager_kms_key_id()
717+
kms_policy = self._get_kms_decrypt_policy(self.SecretsManagerKmsKeyId)
718+
statements.append(kms_policy)
719+
720+
document = {
721+
"PolicyName": "SamAutoGeneratedDocumentDBPolicy",
722+
"PolicyDocument": {"Statement": statements},
723+
}
724+
725+
return [document]

samtranslator/model/lambda_.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class LambdaEventSourceMapping(Resource):
9595
resource_type = "AWS::Lambda::EventSourceMapping"
9696
property_types = {
9797
"BatchSize": PropertyType(False, is_type(int)),
98+
"DocumentDBEventSourceConfig": PropertyType(False, IS_DICT),
9899
"Enabled": PropertyType(False, is_type(bool)),
99100
"EventSourceArn": PropertyType(False, IS_STR),
100101
"FunctionName": PropertyType(True, IS_STR),

0 commit comments

Comments
 (0)