2323from troposphere import AWSObject
2424from troposphere .certificatemanager import Certificate , DomainValidationOption
2525import json
26+ import logging
2627
2728if TYPE_CHECKING :
2829 from e3 .aws .troposphere import Stack
3334 "GET" , "POST" , "PUT" , "DELETE" , "ANY" , "HEAD" , "OPTIONS" , "PATCH"
3435 ]
3536
37+ logger = logging .getLogger ("e3.aws.troposphere.apigateway" )
38+
3639
3740class AuthorizationType (Enum ):
3841 """Allowed authorization types for ApiGateway routes."""
@@ -43,6 +46,85 @@ class AuthorizationType(Enum):
4346 CUSTOM = "CUSTOM"
4447
4548
49+ class EndpointConfigurationType (Enum ):
50+ """Allowed endpoint configuration types for RestApi ApiGateways."""
51+
52+ REGIONAL = "REGIONAL"
53+ """APIs will be deployed in the current AWS Region"""
54+ EDGE = "EDGE"
55+ """APIs will route requests to the nearest CloudFront Point of Presence"""
56+ PRIVATE = "PRIVATE"
57+ """API will only be accessible from VPCs."""
58+
59+
60+ class IpAddressType (Enum ):
61+ """The type of IP addresses that can invoke the default endpoint of a REST API."""
62+
63+ IPV4 = "ipv4"
64+ """Supports only edge-optimized and Regional API endpoint types"""
65+ DUAL_STACK = "dualstack"
66+ """Supports all API endpoint types."""
67+
68+
69+ class EndpointAccessMode (Enum ):
70+ """Provide additional governance for your APIs."""
71+
72+ BASIC = "BASIC"
73+ """Allow all clients to access the API"""
74+ STRICT = "STRICT"
75+ """Enforce Server Name Indication (SNI) validation"""
76+
77+
78+ class SecurityPolicy (Enum ):
79+ """The Transport Layer Security (TLS) version + cipher suite for a RestApi."""
80+
81+ SECURITYPOLICY_TLS12_2018_EDGE = "SecurityPolicy_TLS12_2018_EDGE"
82+ SECURITYPOLICY_TLS12_PFS_2025_EDGE = "SecurityPolicy_TLS12_PFS_2025_EDGE"
83+ SECURITYPOLICY_TLS13_1_2_2021_06 = "SecurityPolicy_TLS13_1_2_2021_06"
84+ SECURITYPOLICY_TLS13_1_2_FIPS_PQ_2025_09 = (
85+ "SecurityPolicy_TLS13_1_2_FIPS_PQ_2025_09"
86+ )
87+ SECURITYPOLICY_TLS13_1_2_PFS_PQ_2025_09 = "SecurityPolicy_TLS13_1_2_PFS_PQ_2025_09"
88+ SECURITYPOLICY_TLS13_1_2_PQ_2025_09 = "SecurityPolicy_TLS13_1_2_PQ_2025_09"
89+ SECURITYPOLICY_TLS13_1_3_2025_09 = "SecurityPolicy_TLS13_1_3_2025_09"
90+ SECURITYPOLICY_TLS13_1_3_FIPS_2025_09 = "SecurityPolicy_TLS13_1_3_FIPS_2025_09"
91+ SECURITYPOLICY_TLS13_2025_EDGE = "SecurityPolicy_TLS13_2025_EDGE"
92+ TLS_1_0 = "TLS_1_0"
93+ TLS_1_2 = "TLS_1_2"
94+
95+
96+ LEGACY_SECURITY_POLICIES = {SecurityPolicy .TLS_1_0 , SecurityPolicy .TLS_1_2 }
97+
98+ SecurityPolicyLookup = {
99+ EndpointConfigurationType .REGIONAL : {
100+ SecurityPolicy .SECURITYPOLICY_TLS13_1_2_2021_06 ,
101+ SecurityPolicy .SECURITYPOLICY_TLS13_1_2_FIPS_PQ_2025_09 ,
102+ SecurityPolicy .SECURITYPOLICY_TLS13_1_2_PFS_PQ_2025_09 ,
103+ SecurityPolicy .SECURITYPOLICY_TLS13_1_2_PQ_2025_09 ,
104+ SecurityPolicy .SECURITYPOLICY_TLS13_1_3_2025_09 ,
105+ SecurityPolicy .SECURITYPOLICY_TLS13_1_3_FIPS_2025_09 ,
106+ SecurityPolicy .TLS_1_0 ,
107+ SecurityPolicy .TLS_1_2 ,
108+ },
109+ EndpointConfigurationType .EDGE : {
110+ SecurityPolicy .SECURITYPOLICY_TLS12_2018_EDGE ,
111+ SecurityPolicy .SECURITYPOLICY_TLS12_PFS_2025_EDGE ,
112+ SecurityPolicy .SECURITYPOLICY_TLS13_2025_EDGE ,
113+ SecurityPolicy .TLS_1_0 ,
114+ SecurityPolicy .TLS_1_2 ,
115+ },
116+ EndpointConfigurationType .PRIVATE : {
117+ SecurityPolicy .SECURITYPOLICY_TLS13_1_2_2021_06 ,
118+ SecurityPolicy .SECURITYPOLICY_TLS13_1_2_FIPS_PQ_2025_09 ,
119+ SecurityPolicy .SECURITYPOLICY_TLS13_1_2_PFS_PQ_2025_09 ,
120+ SecurityPolicy .SECURITYPOLICY_TLS13_1_2_PQ_2025_09 ,
121+ SecurityPolicy .SECURITYPOLICY_TLS13_1_3_2025_09 ,
122+ SecurityPolicy .SECURITYPOLICY_TLS13_1_3_FIPS_2025_09 ,
123+ SecurityPolicy .TLS_1_2 ,
124+ },
125+ }
126+
127+
46128# Declare some constants to make declarations more concise.
47129NO_AUTH = AuthorizationType .NONE
48130JWT_AUTH = AuthorizationType .JWT
@@ -733,6 +815,11 @@ def __init__(
733815 policy : list [PolicyStatement ] | None = None ,
734816 minimum_compression_size : int | None = None ,
735817 binary_media_types : list [str ] | None = None ,
818+ endpoint_configuration_type : EndpointConfigurationType | None = None ,
819+ ip_address_type : IpAddressType | None = None ,
820+ endpoint_access_mode : EndpointAccessMode | None = None ,
821+ security_policy : SecurityPolicy | None = None ,
822+ integration_timeout : int | None = None ,
736823 ):
737824 """Initialize a Rest API.
738825
@@ -777,6 +864,16 @@ def __init__(
777864 bytes, inclusive) or disable compression (with a null value) on an API
778865 :param binary_media_types: the list of binary media types supported by
779866 the RestApi
867+ :param endpoint_configuration_type: the endpoint configuration type for the API
868+ :param ip_address_type: the type of IP addresses that can invoke the
869+ default endpoint for your API.
870+ :param endpoint_access_mode: Provide additional governance for the API
871+ :param security_policy: determines the TLS version & cipher suite
872+ supported by the API
873+ :param integration_timeout: integration timeout in ms (50-29000 by
874+ default, can be increased for Regional/Private APIs with
875+ quota increase). If None, uses API Gateway default (29000ms)
876+
780877 """
781878 super ().__init__ (
782879 name = name ,
@@ -796,6 +893,11 @@ def __init__(
796893 self .policy = policy
797894 self .minimum_compression_size = minimum_compression_size
798895 self .binary_media_types = binary_media_types
896+ self .endpoint_configuration_type = endpoint_configuration_type
897+ self .ip_address_type = ip_address_type
898+ self .endpoint_access_mode = endpoint_access_mode
899+ self .security_policy = security_policy
900+ self .integration_timeout = integration_timeout
799901
800902 # For backward compatibility
801903 if resource_list is None :
@@ -950,15 +1052,13 @@ def _declare_method(
9501052 self .lambda_arn if resource_lambda_arn is None else resource_lambda_arn
9511053 )
9521054
953- integration = apigateway .Integration (
954- f"{ id_prefix } Integration" ,
955- # set at POST because we are doing lambda integration
956- CacheKeyParameters = [],
957- CacheNamespace = "none" ,
958- IntegrationHttpMethod = "POST" ,
959- PassthroughBehavior = "NEVER" ,
960- Type = "AWS_PROXY" ,
961- Uri = (
1055+ integration_params = {
1056+ "CacheKeyParameters" : [],
1057+ "CacheNamespace" : "none" ,
1058+ "IntegrationHttpMethod" : "POST" ,
1059+ "PassthroughBehavior" : "NEVER" ,
1060+ "Type" : "AWS_PROXY" ,
1061+ "Uri" : (
9621062 integration_uri
9631063 if integration_uri is not None
9641064 else Sub (
@@ -967,6 +1067,15 @@ def _declare_method(
9671067 dict_values = {"lambdaArn" : lambda_arn },
9681068 )
9691069 ),
1070+ }
1071+
1072+ # Add timeout if specified
1073+ if self .integration_timeout is not None :
1074+ integration_params ["TimeoutInMillis" ] = self .integration_timeout
1075+
1076+ integration = apigateway .Integration (
1077+ f"{ id_prefix } Integration" ,
1078+ ** integration_params ,
9701079 )
9711080
9721081 method_params = {
@@ -1012,6 +1121,21 @@ def _declare_method(
10121121 )
10131122 return result
10141123
1124+ @cached_property
1125+ def _endpoint_configuration (self ) -> apigateway .EndpointConfiguration | None :
1126+ """Get the endpoint configuration for the Rest API.
1127+
1128+ :return: endpoint configuration or None
1129+ """
1130+ if self .endpoint_configuration_type is None and self .ip_address_type is None :
1131+ return None
1132+ params : dict [str , str | list [str ]] = {}
1133+ if self .endpoint_configuration_type is not None :
1134+ params ["Types" ] = [self .endpoint_configuration_type .value ]
1135+ if self .ip_address_type is not None :
1136+ params ["IpAddressType" ] = self .ip_address_type .value
1137+ return apigateway .EndpointConfiguration (** params )
1138+
10151139 def _declare_domain_name (
10161140 self , domain_name : str , certificate_arn : Ref | str
10171141 ) -> apigatewayv2 .DomainName | apigateway .DomainName :
@@ -1021,10 +1145,21 @@ def _declare_domain_name(
10211145 :param certificate_arn: the ARN of the certificate
10221146 :return: a domain name aws resource
10231147 """
1148+ params = {"DomainName" : domain_name }
1149+ if (
1150+ self .endpoint_configuration_type == EndpointConfigurationType .REGIONAL
1151+ and self ._endpoint_configuration is not None
1152+ ):
1153+ params ["RegionalCertificateArn" ] = certificate_arn
1154+ params ["EndpointConfiguration" ] = self ._endpoint_configuration
1155+ else :
1156+ params ["CertificateArn" ] = certificate_arn
1157+ if self .security_policy is not None :
1158+ params ["SecurityPolicy" ] = self .security_policy .value
1159+ if self .endpoint_access_mode is not None :
1160+ params ["EndpointAccessMode" ] = self .endpoint_access_mode .value
10241161 return apigateway .DomainName (
1025- name_to_id (self .name + domain_name + "Domain" ),
1026- DomainName = domain_name ,
1027- CertificateArn = certificate_arn ,
1162+ name_to_id (self .name + domain_name + "Domain" ), ** params
10281163 )
10291164
10301165 def _declare_api_mapping (
@@ -1160,6 +1295,11 @@ def _declare_resources(
11601295
11611296 def _get_alias_target_attributes (self ) -> Api ._AliasTargetAttributes :
11621297 """Get atributes to pass to GetAtt for alias target."""
1298+ if self .endpoint_configuration_type == EndpointConfigurationType .REGIONAL :
1299+ return {
1300+ "DNSName" : "RegionalDomainName" ,
1301+ "HostedZoneId" : "RegionalHostedZoneId" ,
1302+ }
11631303 return {
11641304 "DNSName" : "DistributionDomainName" ,
11651305 "HostedZoneId" : "DistributionHostedZoneId" ,
@@ -1217,6 +1357,27 @@ def resources(self, stack: Stack) -> list[AWSObject]:
12171357 }
12181358 if self .policy :
12191359 api_params ["Policy" ] = PolicyDocument (statements = self .policy ).as_dict
1360+ if self .endpoint_access_mode is not None :
1361+ api_params ["EndpointAccessMode" ] = self .endpoint_access_mode .value
1362+ if self .security_policy is not None :
1363+ api_params ["SecurityPolicy" ] = self .security_policy .value
1364+ if self .security_policy in LEGACY_SECURITY_POLICIES :
1365+ logger .warning (
1366+ f"{ self .security_policy .value } is a legacy security policy. "
1367+ "Consider using one that starts with 'SecurityPolicy' instead"
1368+ )
1369+ if (
1370+ self .endpoint_configuration_type is not None
1371+ and self .security_policy
1372+ not in SecurityPolicyLookup [self .endpoint_configuration_type ]
1373+ ):
1374+ logger .warning (
1375+ f"{ self .security_policy .value } security policy may not be "
1376+ f"compatible with { self .endpoint_configuration_type .value } "
1377+ "endpoint configuration type"
1378+ )
1379+ if self ._endpoint_configuration is not None :
1380+ api_params ["EndpointConfiguration" ] = self ._endpoint_configuration
12201381
12211382 if self .minimum_compression_size is not None :
12221383 api_params ["MinimumCompressionSize" ] = self .minimum_compression_size
0 commit comments