cdk-factory 0.15.10__py3-none-any.whl → 0.18.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cdk-factory might be problematic. Click here for more details.
- cdk_factory/configurations/base_config.py +23 -24
- cdk_factory/configurations/cdk_config.py +6 -4
- cdk_factory/configurations/deployment.py +12 -0
- cdk_factory/configurations/devops.py +1 -1
- cdk_factory/configurations/pipeline_stage.py +29 -5
- cdk_factory/configurations/resources/acm.py +85 -0
- cdk_factory/configurations/resources/auto_scaling.py +7 -5
- cdk_factory/configurations/resources/cloudfront.py +7 -2
- cdk_factory/configurations/resources/ecr.py +1 -1
- cdk_factory/configurations/resources/ecs_cluster.py +108 -0
- cdk_factory/configurations/resources/ecs_service.py +17 -2
- cdk_factory/configurations/resources/load_balancer.py +17 -4
- cdk_factory/configurations/resources/monitoring.py +8 -3
- cdk_factory/configurations/resources/rds.py +305 -19
- cdk_factory/configurations/resources/rum.py +7 -2
- cdk_factory/configurations/resources/s3.py +1 -1
- cdk_factory/configurations/resources/security_group_full_stack.py +7 -8
- cdk_factory/configurations/resources/vpc.py +19 -0
- cdk_factory/configurations/workload.py +32 -2
- cdk_factory/constructs/ecr/ecr_construct.py +9 -2
- cdk_factory/constructs/lambdas/policies/policy_docs.py +4 -4
- cdk_factory/interfaces/istack.py +6 -3
- cdk_factory/interfaces/networked_stack_mixin.py +75 -0
- cdk_factory/interfaces/standardized_ssm_mixin.py +657 -0
- cdk_factory/interfaces/vpc_provider_mixin.py +210 -0
- cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
- cdk_factory/pipeline/pipeline_factory.py +222 -27
- cdk_factory/stack/stack_factory.py +34 -0
- cdk_factory/stack_library/__init__.py +3 -2
- cdk_factory/stack_library/acm/__init__.py +6 -0
- cdk_factory/stack_library/acm/acm_stack.py +169 -0
- cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
- cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +366 -408
- cdk_factory/stack_library/code_artifact/code_artifact_stack.py +2 -2
- cdk_factory/stack_library/cognito/cognito_stack.py +152 -92
- cdk_factory/stack_library/dynamodb/dynamodb_stack.py +19 -15
- cdk_factory/stack_library/ecr/ecr_stack.py +2 -2
- cdk_factory/stack_library/ecs/__init__.py +12 -0
- cdk_factory/stack_library/ecs/ecs_cluster_stack.py +316 -0
- cdk_factory/stack_library/ecs/ecs_service_stack.py +20 -39
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +2 -2
- cdk_factory/stack_library/load_balancer/load_balancer_stack.py +151 -118
- cdk_factory/stack_library/rds/rds_stack.py +85 -74
- cdk_factory/stack_library/route53/route53_stack.py +8 -3
- cdk_factory/stack_library/rum/rum_stack.py +108 -91
- cdk_factory/stack_library/security_group/security_group_full_stack.py +9 -22
- cdk_factory/stack_library/security_group/security_group_stack.py +11 -11
- cdk_factory/stack_library/stack_base.py +5 -0
- cdk_factory/stack_library/vpc/vpc_stack.py +272 -124
- cdk_factory/stack_library/websites/static_website_stack.py +1 -1
- cdk_factory/utilities/api_gateway_integration_utility.py +24 -16
- cdk_factory/utilities/environment_services.py +5 -5
- cdk_factory/utilities/json_loading_utility.py +12 -3
- cdk_factory/validation/config_validator.py +483 -0
- cdk_factory/version.py +1 -1
- cdk_factory/workload/workload_factory.py +1 -0
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/METADATA +1 -1
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/RECORD +61 -54
- cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
- cdk_factory/interfaces/ssm_parameter_mixin.py +0 -329
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/WHEEL +0 -0
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/entry_points.txt +0 -0
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VPC Provider Mixin - Reusable VPC resolution functionality
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, List, Any
|
|
8
|
+
from aws_lambda_powertools import Logger
|
|
9
|
+
from aws_cdk import aws_ec2 as ec2, aws_ssm as ssm
|
|
10
|
+
from constructs import Construct
|
|
11
|
+
|
|
12
|
+
logger = Logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VPCProviderMixin:
|
|
16
|
+
"""
|
|
17
|
+
Mixin class that provides reusable VPC resolution functionality for stacks.
|
|
18
|
+
|
|
19
|
+
This mixin eliminates code duplication across stacks that need to resolve
|
|
20
|
+
VPC references, providing a standardized way to handle:
|
|
21
|
+
- SSM imported VPC parameters (works with enhanced StandardizedSsmMixin)
|
|
22
|
+
- Configuration-based VPC resolution
|
|
23
|
+
- Workload-level VPC fallback
|
|
24
|
+
- Error handling and validation
|
|
25
|
+
|
|
26
|
+
Note: This mixin does NOT handle SSM imports directly - it expects
|
|
27
|
+
the SSM values to be available via the enhanced StandardizedSsmMixin.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def _initialize_vpc_cache(self) -> None:
|
|
31
|
+
"""Initialize the VPC cache attribute"""
|
|
32
|
+
if not hasattr(self, '_vpc'):
|
|
33
|
+
self._vpc: Optional[ec2.IVpc] = None
|
|
34
|
+
|
|
35
|
+
def resolve_vpc(
|
|
36
|
+
self,
|
|
37
|
+
config: Any,
|
|
38
|
+
deployment: Any,
|
|
39
|
+
workload: Any,
|
|
40
|
+
availability_zones: Optional[List[str]] = None
|
|
41
|
+
) -> ec2.IVpc:
|
|
42
|
+
"""
|
|
43
|
+
Resolve VPC from multiple sources with standardized priority order.
|
|
44
|
+
|
|
45
|
+
Priority order:
|
|
46
|
+
1. SSM imported VPC ID (from config.ssm.imports)
|
|
47
|
+
2. Config-level VPC ID
|
|
48
|
+
3. Workload-level VPC ID
|
|
49
|
+
4. Raise error if none found
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
config: The stack configuration
|
|
53
|
+
deployment: The deployment configuration
|
|
54
|
+
workload: The workload configuration
|
|
55
|
+
availability_zones: Optional AZ list for VPC attributes
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Resolved VPC reference
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: If no VPC configuration is found
|
|
62
|
+
"""
|
|
63
|
+
if self._vpc:
|
|
64
|
+
return self._vpc
|
|
65
|
+
|
|
66
|
+
# Default availability zones if not provided - use region-appropriate defaults
|
|
67
|
+
if not availability_zones:
|
|
68
|
+
region = getattr(deployment, 'region', 'us-east-1')
|
|
69
|
+
availability_zones = self._get_default_azs_for_region(region)
|
|
70
|
+
|
|
71
|
+
# Check SSM imports directly from config (source of truth)
|
|
72
|
+
ssm_config = getattr(config, 'ssm', {})
|
|
73
|
+
ssm_imports = ssm_config.get('imports', {})
|
|
74
|
+
|
|
75
|
+
if ssm_imports and "vpc_id" in ssm_imports:
|
|
76
|
+
vpc_id = ssm_imports["vpc_id"]
|
|
77
|
+
|
|
78
|
+
# Get subnet IDs first to determine AZ count
|
|
79
|
+
subnet_ids = []
|
|
80
|
+
if "subnet_ids" in ssm_imports:
|
|
81
|
+
imported_subnets = ssm_imports["subnet_ids"]
|
|
82
|
+
if isinstance(imported_subnets, str):
|
|
83
|
+
subnet_ids = [s.strip() for s in imported_subnets.split(",") if s.strip()]
|
|
84
|
+
elif isinstance(imported_subnets, list):
|
|
85
|
+
subnet_ids = imported_subnets
|
|
86
|
+
|
|
87
|
+
# Adjust availability zones to match subnet count
|
|
88
|
+
if subnet_ids and availability_zones:
|
|
89
|
+
region = getattr(deployment, 'region', 'us-east-1')
|
|
90
|
+
region_code = region.split('-')[0] + '-' + region.split('-')[1]
|
|
91
|
+
availability_zones = [f"{region_code}{chr(97+i)}" for i in range(len(subnet_ids))]
|
|
92
|
+
|
|
93
|
+
return self._create_vpc_from_ssm(vpc_id, availability_zones, subnet_ids if subnet_ids else None)
|
|
94
|
+
|
|
95
|
+
# Check config-level VPC ID
|
|
96
|
+
if hasattr(config, 'vpc_id') and config.vpc_id:
|
|
97
|
+
return ec2.Vpc.from_lookup(self, f"{self.stack_name}-VPC", vpc_id=config.vpc_id)
|
|
98
|
+
|
|
99
|
+
# Check workload-level VPC ID
|
|
100
|
+
if hasattr(workload, 'vpc_id') and workload.vpc_id:
|
|
101
|
+
return ec2.Vpc.from_lookup(self, f"{self.stack_name}-VPC", vpc_id=workload.vpc_id)
|
|
102
|
+
|
|
103
|
+
# No VPC found - raise descriptive error
|
|
104
|
+
raise self._create_vpc_not_found_error(config, workload)
|
|
105
|
+
|
|
106
|
+
def _create_vpc_from_ssm(
|
|
107
|
+
self,
|
|
108
|
+
vpc_id: str,
|
|
109
|
+
availability_zones: List[str],
|
|
110
|
+
subnet_ids: Optional[List[str]] = None
|
|
111
|
+
) -> ec2.IVpc:
|
|
112
|
+
"""
|
|
113
|
+
Create VPC reference from SSM imported VPC ID.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
vpc_id: The VPC ID from SSM (can be SSM path or actual VPC ID)
|
|
117
|
+
availability_zones: List of availability zones
|
|
118
|
+
subnet_ids: Optional list of subnet IDs from SSM
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
VPC reference created from attributes
|
|
122
|
+
"""
|
|
123
|
+
# Check if vpc_id is an SSM path (starts with /) or actual VPC ID
|
|
124
|
+
if vpc_id.startswith('/'):
|
|
125
|
+
# Create CDK token for VPC ID from SSM parameter
|
|
126
|
+
vpc_id_token = ssm.StringParameter.from_string_parameter_name(
|
|
127
|
+
self, f"{self.stack_name}-VPC-ID-Token", vpc_id
|
|
128
|
+
).string_value
|
|
129
|
+
else:
|
|
130
|
+
# Use the VPC ID directly (for testing or direct configuration)
|
|
131
|
+
vpc_id_token = vpc_id
|
|
132
|
+
|
|
133
|
+
# Build VPC attributes
|
|
134
|
+
vpc_attrs = {
|
|
135
|
+
"vpc_id": vpc_id_token,
|
|
136
|
+
"availability_zones": availability_zones,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# If we have subnet_ids from SSM, add them to the attributes
|
|
140
|
+
if subnet_ids:
|
|
141
|
+
# Use the actual subnet IDs from SSM
|
|
142
|
+
vpc_attrs["public_subnet_ids"] = subnet_ids
|
|
143
|
+
|
|
144
|
+
# Use from_vpc_attributes() for SSM tokens with unique construct name
|
|
145
|
+
self._vpc = ec2.Vpc.from_vpc_attributes(self, f"{self.stack_name}-VPC", **vpc_attrs)
|
|
146
|
+
return self._vpc
|
|
147
|
+
|
|
148
|
+
def _create_vpc_not_found_error(self, config: Any, workload: Any) -> ValueError:
|
|
149
|
+
"""
|
|
150
|
+
Create a descriptive error message for missing VPC configuration.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
config: The stack configuration
|
|
154
|
+
workload: The workload configuration
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
ValueError with descriptive message
|
|
158
|
+
"""
|
|
159
|
+
config_name = getattr(config, 'name', 'unknown')
|
|
160
|
+
workload_name = getattr(workload, 'name', 'unknown')
|
|
161
|
+
|
|
162
|
+
return ValueError(
|
|
163
|
+
f"VPC is not defined in the configuration for {config_name}. "
|
|
164
|
+
f"You can provide it at the following locations:\n"
|
|
165
|
+
f" 1. As an SSM import: config.ssm_imports.vpc_id\n"
|
|
166
|
+
f" 2. At the config level: config.vpc_id\n"
|
|
167
|
+
f" 3. At the workload level: workload.vpc_id\n"
|
|
168
|
+
f"Current workload: {workload_name}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def get_vpc_property(self, config: Any, deployment: Any, workload: Any) -> ec2.IVpc:
|
|
172
|
+
"""
|
|
173
|
+
Standard VPC property implementation that can be used by stacks.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
config: The stack configuration
|
|
177
|
+
deployment: The deployment configuration
|
|
178
|
+
workload: The workload configuration
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Resolved VPC reference
|
|
182
|
+
"""
|
|
183
|
+
return self.resolve_vpc(config, deployment, workload)
|
|
184
|
+
|
|
185
|
+
def _get_default_azs_for_region(self, region: str) -> List[str]:
|
|
186
|
+
"""
|
|
187
|
+
Get default availability zones for a given region.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
region: AWS region name (e.g., 'us-east-1', 'us-west-2')
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
List of availability zone names for the region
|
|
194
|
+
"""
|
|
195
|
+
# Common AZ mappings for major AWS regions
|
|
196
|
+
region_az_map = {
|
|
197
|
+
'us-east-1': ['us-east-1a', 'us-east-1b'],
|
|
198
|
+
'us-east-2': ['us-east-2a', 'us-east-2b'],
|
|
199
|
+
'us-west-1': ['us-west-1a', 'us-west-1b'],
|
|
200
|
+
'us-west-2': ['us-west-2a', 'us-west-2b'],
|
|
201
|
+
'eu-west-1': ['eu-west-1a', 'eu-west-1b'],
|
|
202
|
+
'eu-west-2': ['eu-west-2a', 'eu-west-2b'],
|
|
203
|
+
'eu-central-1': ['eu-central-1a', 'eu-central-1b'],
|
|
204
|
+
'ap-southeast-1': ['ap-southeast-1a', 'ap-southeast-1b'],
|
|
205
|
+
'ap-southeast-2': ['ap-southeast-2a', 'ap-southeast-2b'],
|
|
206
|
+
'ap-northeast-1': ['ap-northeast-1a', 'ap-northeast-1b'],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# Return region-specific AZs if available, otherwise fall back to us-east-1
|
|
210
|
+
return region_az_map.get(region, ['us-east-1a', 'us-east-1b'])
|
|
@@ -11,11 +11,18 @@ import ipaddress
|
|
|
11
11
|
import boto3
|
|
12
12
|
from functools import lru_cache
|
|
13
13
|
|
|
14
|
+
# Simple logging for Lambda@Edge (since we can't use external logging libraries)
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
# Configure basic logging
|
|
18
|
+
logger = logging.getLogger()
|
|
19
|
+
logger.setLevel(logging.INFO)
|
|
20
|
+
|
|
14
21
|
# SSM client - will be created in the region where the function executes
|
|
15
22
|
ssm = None
|
|
16
23
|
|
|
17
24
|
@lru_cache(maxsize=128)
|
|
18
|
-
def get_ssm_parameter(parameter_name: str, region: str =
|
|
25
|
+
def get_ssm_parameter(parameter_name: str, region: str = None, default: str = None) -> str:
|
|
19
26
|
"""
|
|
20
27
|
Fetch SSM parameter with caching.
|
|
21
28
|
Lambda@Edge cannot use environment variables, so we fetch from SSM.
|
|
@@ -23,14 +30,18 @@ def get_ssm_parameter(parameter_name: str, region: str = 'us-east-1', default: s
|
|
|
23
30
|
The sentinel value 'NONE' indicates an explicitly unset/disabled parameter.
|
|
24
31
|
|
|
25
32
|
Args:
|
|
26
|
-
parameter_name:
|
|
27
|
-
region: AWS region (
|
|
28
|
-
default: Default value
|
|
33
|
+
parameter_name: SSM parameter name
|
|
34
|
+
region: AWS region (defaults to us-east-1 for Lambda@Edge compatibility)
|
|
35
|
+
default: Default value if parameter not found
|
|
29
36
|
|
|
30
37
|
Returns:
|
|
31
38
|
Parameter value, default value if parameter not found, or empty string if value is 'NONE'
|
|
32
39
|
"""
|
|
33
40
|
global ssm
|
|
41
|
+
# Default to us-east-1 for Lambda@Edge compatibility if no region specified
|
|
42
|
+
if region is None:
|
|
43
|
+
region = 'us-east-1'
|
|
44
|
+
|
|
34
45
|
if ssm is None:
|
|
35
46
|
ssm = boto3.client('ssm', region_name=region)
|
|
36
47
|
|
|
@@ -40,20 +51,20 @@ def get_ssm_parameter(parameter_name: str, region: str = 'us-east-1', default: s
|
|
|
40
51
|
|
|
41
52
|
# Treat 'NONE' sentinel as empty/unset
|
|
42
53
|
if value == 'NONE':
|
|
43
|
-
|
|
54
|
+
logger.info(f"SSM parameter {parameter_name} is set to 'NONE' (explicitly disabled)")
|
|
44
55
|
return ''
|
|
45
56
|
|
|
46
57
|
return value
|
|
47
58
|
except ssm.exceptions.ParameterNotFound:
|
|
48
59
|
if default is not None:
|
|
49
|
-
|
|
60
|
+
logger.info(f"SSM parameter {parameter_name} not found, using default: {default}")
|
|
50
61
|
return default
|
|
51
|
-
|
|
62
|
+
logger.error(f"SSM parameter {parameter_name} not found and no default provided")
|
|
52
63
|
raise
|
|
53
64
|
except Exception as e:
|
|
54
|
-
|
|
65
|
+
logger.error(f"Error fetching SSM parameter {parameter_name}: {str(e)}")
|
|
55
66
|
if default is not None:
|
|
56
|
-
|
|
67
|
+
logger.info(f"Returning default value due to error: {default}")
|
|
57
68
|
return default
|
|
58
69
|
raise
|
|
59
70
|
|
|
@@ -89,7 +100,7 @@ def is_ip_allowed(client_ip: str, allowed_cidrs: list) -> bool:
|
|
|
89
100
|
try:
|
|
90
101
|
client_ip_obj = ipaddress.ip_address(client_ip)
|
|
91
102
|
except ValueError as e:
|
|
92
|
-
|
|
103
|
+
logger.error(f"Invalid client IP address: {e}")
|
|
93
104
|
return False
|
|
94
105
|
|
|
95
106
|
# Check each CIDR individually, skipping invalid ones
|
|
@@ -100,7 +111,7 @@ def is_ip_allowed(client_ip: str, allowed_cidrs: list) -> bool:
|
|
|
100
111
|
return True
|
|
101
112
|
except ValueError as e:
|
|
102
113
|
# Invalid CIDR, log and continue checking others
|
|
103
|
-
|
|
114
|
+
logger.warning(f"Invalid CIDR '{cidr}': {e}")
|
|
104
115
|
continue
|
|
105
116
|
|
|
106
117
|
return False
|
|
@@ -130,24 +141,23 @@ def lambda_handler(event, context):
|
|
|
130
141
|
env = runtime_config.get('environment', 'dev')
|
|
131
142
|
function_base_name = runtime_config.get('function_name', 'ip-gate')
|
|
132
143
|
|
|
133
|
-
|
|
144
|
+
logger.info(f"Runtime config loaded: environment={env}, function_name={function_base_name}")
|
|
134
145
|
except FileNotFoundError:
|
|
135
146
|
# Fallback: extract from Lambda context (less reliable)
|
|
136
|
-
|
|
147
|
+
logger.warning("runtime_config.json not found, falling back to function name parsing")
|
|
137
148
|
function_full_name = context.function_name
|
|
138
149
|
|
|
139
150
|
# Parse environment from function name as fallback
|
|
140
151
|
parts = function_full_name.split('-')
|
|
141
152
|
common_envs = ['dev', 'prod', 'staging', 'test', 'qa', 'uat']
|
|
142
|
-
env = 'dev'
|
|
143
|
-
|
|
153
|
+
env = 'dev' # Default fallback
|
|
144
154
|
for part in parts:
|
|
145
155
|
if part in common_envs:
|
|
146
156
|
env = part
|
|
147
157
|
break
|
|
148
158
|
|
|
149
159
|
function_base_name = 'ip-gate'
|
|
150
|
-
|
|
160
|
+
logger.info(f"Fallback: environment={env}, function_name={function_base_name}")
|
|
151
161
|
|
|
152
162
|
# Full function name for SSM paths
|
|
153
163
|
# Lambda@Edge replicas prepend region (e.g., "us-east-1.tech-talk-dev-ip-gate")
|
|
@@ -156,46 +166,46 @@ def lambda_handler(event, context):
|
|
|
156
166
|
if '.' in function_name and function_name.split('.')[0].startswith('us-'):
|
|
157
167
|
# Strip region prefix (e.g., "us-east-1." -> "tech-talk-dev-ip-gate")
|
|
158
168
|
function_name = '.'.join(function_name.split('.')[1:])
|
|
159
|
-
|
|
169
|
+
logger.info(f"Stripped region prefix from function name: {function_name}")
|
|
160
170
|
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
logger.info(f"Lambda function ARN: {context.invoked_function_arn}")
|
|
172
|
+
logger.info(f"Using function name for SSM lookups: {function_name}")
|
|
163
173
|
|
|
164
174
|
try:
|
|
165
175
|
# Fetch configuration from SSM Parameter Store
|
|
166
176
|
# Auto-generated paths: /{env}/{function-name}/{key}
|
|
167
|
-
gate_enabled = get_ssm_parameter(f'/{env}/{function_name}/gate-enabled', 'us-east-1')
|
|
177
|
+
gate_enabled = get_ssm_parameter(f'/{env}/{function_name}/gate-enabled', region='us-east-1')
|
|
168
178
|
|
|
169
179
|
# If gating is disabled, allow all traffic
|
|
170
180
|
# Empty string (from 'NONE' sentinel) is treated as disabled
|
|
171
181
|
if not gate_enabled or gate_enabled.lower() not in ('true', '1', 'yes'):
|
|
172
|
-
|
|
182
|
+
logger.info(f"IP gating is disabled (GATE_ENABLED={gate_enabled or 'NONE'})")
|
|
173
183
|
return request
|
|
174
184
|
|
|
175
185
|
# Get allowed CIDRs and backup host
|
|
176
|
-
allow_cidrs_str = get_ssm_parameter(f'/{env}/{function_name}/allow-cidrs', 'us-east-1')
|
|
177
|
-
dns_alias = get_ssm_parameter(f'/{env}/{function_name}/dns-alias', 'us-east-1')
|
|
186
|
+
allow_cidrs_str = get_ssm_parameter(f'/{env}/{function_name}/allow-cidrs', region='us-east-1')
|
|
187
|
+
dns_alias = get_ssm_parameter(f'/{env}/{function_name}/dns-alias', region='us-east-1')
|
|
178
188
|
|
|
179
189
|
# Parse allowed CIDRs (empty string results in empty list)
|
|
180
190
|
allowed_cidrs = [cidr.strip() for cidr in allow_cidrs_str.split(',') if cidr.strip()]
|
|
181
191
|
|
|
182
192
|
# Get client IP
|
|
183
193
|
client_ip = get_client_ip(request)
|
|
184
|
-
|
|
194
|
+
logger.info(f"Client IP: {client_ip}")
|
|
185
195
|
|
|
186
196
|
# Check if IP is allowed
|
|
187
197
|
if is_ip_allowed(client_ip, allowed_cidrs):
|
|
188
|
-
|
|
198
|
+
logger.info(f"IP {client_ip} is allowed")
|
|
189
199
|
return request
|
|
190
200
|
|
|
191
201
|
# IP not allowed - either redirect or proxy backup page
|
|
192
202
|
# Check response mode from SSM (default: redirect for backward compatibility)
|
|
193
203
|
response_mode_param = f"/{env}/{function_name}/response-mode"
|
|
194
|
-
response_mode = get_ssm_parameter(response_mode_param, 'us-east-1', default='redirect')
|
|
204
|
+
response_mode = get_ssm_parameter(response_mode_param, region='us-east-1', default='redirect')
|
|
195
205
|
|
|
196
206
|
if response_mode == 'proxy':
|
|
197
207
|
# Proxy mode: Fetch and return backup content (keeps URL the same)
|
|
198
|
-
|
|
208
|
+
logger.info(f"IP {client_ip} is NOT allowed, proxying content from {dns_alias}")
|
|
199
209
|
|
|
200
210
|
try:
|
|
201
211
|
import urllib3
|
|
@@ -230,17 +240,17 @@ def lambda_handler(event, context):
|
|
|
230
240
|
'body': alias_response.data.decode('utf-8')
|
|
231
241
|
}
|
|
232
242
|
|
|
233
|
-
|
|
243
|
+
logger.info(f"Successfully proxied backup page (status {alias_response.status})")
|
|
234
244
|
return response
|
|
235
245
|
|
|
236
246
|
except Exception as proxy_error:
|
|
237
|
-
|
|
247
|
+
logger.error(f"Error proxying backup content: {str(proxy_error)}")
|
|
238
248
|
# Fall back to redirect if proxy fails
|
|
239
|
-
|
|
249
|
+
logger.info(f"Falling back to redirect mode")
|
|
240
250
|
response_mode = 'redirect'
|
|
241
251
|
|
|
242
252
|
# Redirect mode (default): HTTP 302 redirect to backup site
|
|
243
|
-
|
|
253
|
+
logger.info(f"IP {client_ip} is NOT allowed, redirecting to {dns_alias}")
|
|
244
254
|
|
|
245
255
|
response = {
|
|
246
256
|
'status': '302',
|
|
@@ -249,14 +259,6 @@ def lambda_handler(event, context):
|
|
|
249
259
|
'location': [{
|
|
250
260
|
'key': 'Location',
|
|
251
261
|
'value': f'https://{dns_alias}'
|
|
252
|
-
}],
|
|
253
|
-
'cache-control': [{
|
|
254
|
-
'key': 'Cache-Control',
|
|
255
|
-
'value': 'no-cache, no-store, must-revalidate'
|
|
256
|
-
}],
|
|
257
|
-
'x-ip-gate-mode': [{
|
|
258
|
-
'key': 'X-IP-Gate-Mode',
|
|
259
|
-
'value': 'redirect'
|
|
260
262
|
}]
|
|
261
263
|
}
|
|
262
264
|
}
|
|
@@ -264,7 +266,7 @@ def lambda_handler(event, context):
|
|
|
264
266
|
return response
|
|
265
267
|
|
|
266
268
|
except Exception as e:
|
|
267
|
-
|
|
269
|
+
logger.error(f"Error in IP gating function: {str(e)}")
|
|
268
270
|
# On error, allow the request to proceed (fail open)
|
|
269
271
|
# Change this to fail closed if preferred
|
|
270
272
|
return request
|