Skip to content

Commit 63acab0

Browse files
committed
Merge branch 'topic/Add-support-for-regional-rest-apis' into 'master'
Add support for more Rest API Features See merge request it/e3-aws!104
2 parents 16cd99d + 78e3e21 commit 63acab0

File tree

8 files changed

+1414
-12
lines changed

8 files changed

+1414
-12
lines changed

src/e3/aws/troposphere/apigateway/__init__.py

Lines changed: 173 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from troposphere import AWSObject
2424
from troposphere.certificatemanager import Certificate, DomainValidationOption
2525
import json
26+
import logging
2627

2728
if TYPE_CHECKING:
2829
from e3.aws.troposphere import Stack
@@ -33,6 +34,8 @@
3334
"GET", "POST", "PUT", "DELETE", "ANY", "HEAD", "OPTIONS", "PATCH"
3435
]
3536

37+
logger = logging.getLogger("e3.aws.troposphere.apigateway")
38+
3639

3740
class 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.
47129
NO_AUTH = AuthorizationType.NONE
48130
JWT_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

Comments
 (0)