Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from parameterized import parameterized

from integration.helpers.base_test import BaseTest


class TestApiWithDisableExecuteApiEndpoint(BaseTest):
@parameterized.expand(
[
("combination/api_with_disable_execute_api_endpoint", True),
("combination/api_with_disable_execute_api_endpoint", False),
]
)
def test_end_point_configuration(self, file_name, disable_value):
parameters = [
{
"ParameterKey": "DisableExecuteApiEndpointValue",
"ParameterValue": "true" if disable_value else "false",
"UsePreviousValue": False,
"ResolvedValue": "string",
}
]

self.create_and_verify_stack(file_name, parameters)

rest_api_id = self.get_physical_id_by_type("AWS::ApiGateway::RestApi")
apigw_client = self.client_provider.api_client

response = apigw_client.get_rest_api(restApiId=rest_api_id)
api_result = response["disableExecuteApiEndpoint"]
self.assertEqual(api_result, disable_value)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{"LogicalResourceId": "RestApiGateway", "ResourceType": "AWS::ApiGateway::RestApi"},
{"LogicalResourceId": "RestApiGatewayDeployment", "ResourceType": "AWS::ApiGateway::Deployment"},
{"LogicalResourceId": "RestApiGatewayProdStage", "ResourceType": "AWS::ApiGateway::Stage"},
{"LogicalResourceId": "RestApiFunction", "ResourceType": "AWS::Lambda::Function"},
{"LogicalResourceId": "RestApiFunctionIamPermissionProd", "ResourceType": "AWS::Lambda::Permission"},
{"LogicalResourceId": "RestApiFunctionRole", "ResourceType": "AWS::IAM::Role"}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Parameters:
DisableExecuteApiEndpointValue:
Description: Variable to define if client can access default API endpoint.
Type: String
AllowedValues: [true, false]

Resources:
RestApiGateway:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
DisableExecuteApiEndpoint:
Ref: DisableExecuteApiEndpointValue

RestApiFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
exports.handler = async (event) => {
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
Handler: index.handler
Runtime: nodejs12.x
Events:
Iam:
Type: Api
Properties:
RestApiId: !Ref RestApiGateway
Method: GET
Path: /
Outputs:
ApiUrl:
Description: "API endpoint URL for Prod environment"
Value:
Fn::Sub: 'https://${RestApiGateway}.execute-api.${AWS::Region}.${AWS::URLSuffix}/Prod/'
2 changes: 1 addition & 1 deletion samtranslator/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.41.0"
__version__ = "1.42.0"
24 changes: 24 additions & 0 deletions samtranslator/model/api/api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def __init__(
method_settings=None,
binary_media=None,
minimum_compression_size=None,
disable_execute_api_endpoint=None,
cors=None,
auth=None,
gateway_responses=None,
Expand Down Expand Up @@ -218,6 +219,7 @@ def __init__(
self.method_settings = method_settings
self.binary_media = binary_media
self.minimum_compression_size = minimum_compression_size
self.disable_execute_api_endpoint = disable_execute_api_endpoint
self.cors = cors
self.auth = auth
self.gateway_responses = gateway_responses
Expand Down Expand Up @@ -290,8 +292,27 @@ def _construct_rest_api(self):
if self.mode:
rest_api.Mode = self.mode

if self.disable_execute_api_endpoint is not None:
self._add_endpoint_extension()

return rest_api

def _add_endpoint_extension(self):
"""Add disableExecuteApiEndpoint if it is set in SAM
Note:
If neither DefinitionUri nor DefinitionBody are specified,
SAM will generate a openapi definition body based on template configuration.
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-definitionbody
For this reason, we always put DisableExecuteApiEndpoint into openapi object irrespective of origin of DefinitionBody.
"""
if self.disable_execute_api_endpoint and not self.definition_body:
raise InvalidResourceException(
self.logical_id, "DisableExecuteApiEndpoint works only within 'DefinitionBody' property."
)
editor = SwaggerEditor(self.definition_body)
editor.add_disable_execute_api_endpoint_extension(self.disable_execute_api_endpoint)
self.definition_body = editor.swagger

def _construct_body_s3_dict(self):
"""Constructs the RestApi's `BodyS3Location property`_, from the SAM Api's DefinitionUri property.
Expand Down Expand Up @@ -444,6 +465,9 @@ def _construct_api_domain(self, rest_api):
if self.domain.get("SecurityPolicy", None):
domain.SecurityPolicy = self.domain["SecurityPolicy"]

if self.domain.get("OwnershipVerificationCertificateArn", None):
domain.OwnershipVerificationCertificateArn = self.domain["OwnershipVerificationCertificateArn"]

# Create BasepathMappings
if self.domain.get("BasePath") and isinstance(self.domain.get("BasePath"), string_types):
basepaths = [self.domain.get("BasePath")]
Expand Down
6 changes: 6 additions & 0 deletions samtranslator/model/api/http_api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,12 @@ def _construct_api_domain(self, http_api):
"EndpointConfiguration for Custom Domains must be one of {}.".format(["REGIONAL"]),
)
domain_config["EndpointType"] = endpoint

if self.domain.get("OwnershipVerificationCertificateArn", None):
domain_config["OwnershipVerificationCertificateArn"] = self.domain.get(
"OwnershipVerificationCertificateArn"
)

domain_config["CertificateArn"] = self.domain.get("CertificateArn")
if self.domain.get("SecurityPolicy", None):
domain_config["SecurityPolicy"] = self.domain.get("SecurityPolicy")
Expand Down
1 change: 1 addition & 0 deletions samtranslator/model/apigateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ class ApiGatewayDomainName(Resource):
"MutualTlsAuthentication": PropertyType(False, is_type(dict)),
"SecurityPolicy": PropertyType(False, is_str()),
"CertificateArn": PropertyType(False, is_str()),
"OwnershipVerificationCertificateArn": PropertyType(False, is_str()),
}


Expand Down
20 changes: 20 additions & 0 deletions samtranslator/model/eventsources/pull.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class PullEventSource(ResourceMacro):
:cvar str policy_arn: The ARN of the AWS managed role policy corresponding to this pull event source
"""

# Event types that support `FilterCriteria`, stored as a list to keep the alphabetical order
RESOURCE_TYPES_WITH_EVENT_FILTERING = ["DynamoDB", "Kinesis", "SQS"]

resource_type = None
requires_stream_queue_broker = True
property_types = {
Expand All @@ -43,6 +46,7 @@ class PullEventSource(ResourceMacro):
"TumblingWindowInSeconds": PropertyType(False, is_type(int)),
"FunctionResponseTypes": PropertyType(False, is_type(list)),
"KafkaBootstrapServers": PropertyType(False, is_type(list)),
"FilterCriteria": PropertyType(False, is_type(dict)),
}

def get_policy_arn(self):
Expand Down Expand Up @@ -102,6 +106,8 @@ def to_cloudformation(self, **kwargs):
lambda_eventsourcemapping.SourceAccessConfigurations = self.SourceAccessConfigurations
lambda_eventsourcemapping.TumblingWindowInSeconds = self.TumblingWindowInSeconds
lambda_eventsourcemapping.FunctionResponseTypes = self.FunctionResponseTypes
lambda_eventsourcemapping.FilterCriteria = self.FilterCriteria
self._validate_filter_criteria()

if self.KafkaBootstrapServers:
lambda_eventsourcemapping.SelfManagedEventSource = {
Expand Down Expand Up @@ -169,6 +175,20 @@ def _link_policy(self, role, destination_config_policy=None):
if not destination_config_policy.get("PolicyDocument") in [d["PolicyDocument"] for d in role.Policies]:
role.Policies.append(destination_config_policy)

def _validate_filter_criteria(self):
if not self.FilterCriteria or is_intrinsic(self.FilterCriteria):
return
if self.resource_type not in self.RESOURCE_TYPES_WITH_EVENT_FILTERING:
raise InvalidEventException(
self.relative_id,
"FilterCriteria is only available for {} events.".format(
", ".join(self.RESOURCE_TYPES_WITH_EVENT_FILTERING)
),
)
# FilterCriteria is either empty or only has "Filters"
if list(self.FilterCriteria.keys()) not in [[], ["Filters"]]:
raise InvalidEventException(self.relative_id, "FilterCriteria field has a wrong format")


class Kinesis(PullEventSource):
"""Kinesis event source."""
Expand Down
1 change: 1 addition & 0 deletions samtranslator/model/lambda_.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class LambdaEventSourceMapping(Resource):
"TumblingWindowInSeconds": PropertyType(False, is_type(int)),
"FunctionResponseTypes": PropertyType(False, is_type(list)),
"SelfManagedEventSource": PropertyType(False, is_type(dict)),
"FilterCriteria": PropertyType(False, is_type(dict)),
}

runtime_attrs = {"name": lambda self: ref(self.logical_id)}
Expand Down
2 changes: 2 additions & 0 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,7 @@ class SamApi(SamResourceMacro):
"Domain": PropertyType(False, is_type(dict)),
"Description": PropertyType(False, is_str()),
"Mode": PropertyType(False, is_str()),
"DisableExecuteApiEndpoint": PropertyType(False, is_type(bool)),
}

referable_properties = {
Expand Down Expand Up @@ -925,6 +926,7 @@ def to_cloudformation(self, **kwargs):
method_settings=self.MethodSettings,
binary_media=self.BinaryMediaTypes,
minimum_compression_size=self.MinimumCompressionSize,
disable_execute_api_endpoint=self.DisableExecuteApiEndpoint,
cors=self.Cors,
auth=self.Auth,
gateway_responses=self.GatewayResponses,
Expand Down
14 changes: 14 additions & 0 deletions samtranslator/swagger/swagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class SwaggerEditor(object):
_X_ANY_METHOD = "x-amazon-apigateway-any-method"
_X_APIGW_REQUEST_VALIDATORS = "x-amazon-apigateway-request-validators"
_X_APIGW_REQUEST_VALIDATOR = "x-amazon-apigateway-request-validator"
_X_ENDPOINT_CONFIG = "x-amazon-apigateway-endpoint-configuration"
_CACHE_KEY_PARAMETERS = "cacheKeyParameters"
# https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
_ALL_HTTP_METHODS = ["OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"]
Expand Down Expand Up @@ -111,6 +112,19 @@ def get_method_contents(self, method):
return method[self._CONDITIONAL_IF][1:]
return [method]

def add_disable_execute_api_endpoint_extension(self, disable_execute_api_endpoint):
"""Add endpoint configuration to _X_APIGW_ENDPOINT_CONFIG in open api definition as extension
Following this guide:
https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-endpoint-configuration.html
:param boolean disable_execute_api_endpoint: Specifies whether clients can invoke your API by using the default execute-api endpoint.
"""
if not self._doc.get(self._X_ENDPOINT_CONFIG):
self._doc[self._X_ENDPOINT_CONFIG] = {}

DISABLE_EXECUTE_API_ENDPOINT = "disableExecuteApiEndpoint"
set_disable_api_endpoint = {DISABLE_EXECUTE_API_ENDPOINT: disable_execute_api_endpoint}
self._doc[self._X_ENDPOINT_CONFIG].update(set_disable_api_endpoint)

def has_integration(self, path, method):
"""
Checks if an API Gateway integration is already present at the given path/method
Expand Down
12 changes: 12 additions & 0 deletions samtranslator/validator/sam_schema/definitions/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@
"intrinsic"
]
},
"DisableExecuteApiEndpoint": {
"type": [
"boolean",
"intrinsic"
]
},
"Domain": {
"$ref": "#definitions/AWS::Serverless::Api.DomainConfiguration"
},
Expand Down Expand Up @@ -443,6 +449,12 @@
"string",
"intrinsic"
]
},
"OwnershipVerificationCertificateArn": {
"type": [
"string",
"intrinsic"
]
}
},
"required": [
Expand Down
56 changes: 56 additions & 0 deletions tests/translator/input/error_event_filtering.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
Resources:
WrongFilterName:
Type: AWS::Serverless::Function
Properties:
CodeUri: s3://sam-demo-bucket/filtered_events.zip
Handler: index.handler
Runtime: nodejs16.x
Events:
DynamoDBStreamEvent:
Type: DynamoDB
Properties:
Stream: !GetAtt DynamoDBTable.StreamArn
StartingPosition: TRIM_HORIZON
FilterCriteria:
FiltersToUse:
- Pattern: '{"name": "value"}'

NotSupportedPullEvent:
Type: AWS::Serverless::Function
Properties:
CodeUri: s3://sam-demo-bucket/filtered_events.zip
Handler: index.handler
Runtime: nodejs16.x
Events:
KafkaEvent:
Type: MSK
Properties:
StartingPosition: LATEST
Stream: arn:aws:kafka:us-east-1:012345678012:cluster/clusterName/abcdefab-1234-abcd-5678-cdef0123ab01-2
Topics:
- MyTopic
FilterCriteria:
Filters:
- Pattern: '{"name": "value"}'


NotSupportedPushEvent:
Type: AWS::Serverless::Function
Properties:
CodeUri: s3://sam-demo-bucket/filtered_events.zip
Handler: index.handler
Runtime: nodejs16.x
Events:
SNSEvent:
Type: SNS
Properties:
Topic: !GetAtt MySnsTopic.Arn
FilterCriteria:
Filters:
- Pattern: '{"name": "value"}'

DynamoDBTable:
Type: AWS::DynamoDB::Table

MySnsTopic:
Type: AWS::SNS::Topic
49 changes: 49 additions & 0 deletions tests/translator/input/function_with_event_filtering.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Resources:
FilteredEventsFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: s3://sam-demo-bucket/filtered_events.zip
Handler: index.handler
Runtime: nodejs16.x
Events:
KinesisStream:
Type: Kinesis
Properties:
Stream: !GetAtt KinesisStream.Arn
StartingPosition: LATEST
FilterCriteria:
Filters:
- Pattern: '{"name": "value"}'
- Pattern: '{"name2": "value2"}'
DynamoDBStreamEvent:
Type: DynamoDB
Properties:
Stream: !GetAtt DynamoDBTable.StreamArn
StartingPosition: TRIM_HORIZON
FilterCriteria:
Filters:
- Pattern: '{
"dynamodb": {
"NewImage": {
"value": { "S": ["test"] }
}
}
}'
MySqsQueue:
Type: SQS
Properties:
Queue: !GetAtt MySqsQueue.Arn
FilterCriteria:
Filters:
- Pattern: '{"name": "value"}'

KinesisStream:
Type: AWS::Kinesis::Stream
Properties:
ShardCount: 1

DynamoDBTable:
Type: AWS::DynamoDB::Table

MySqsQueue:
Type: AWS::SQS::Queue
Loading