cdk-factory 0.13.0__py3-none-any.whl → 0.13.2__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.

@@ -1,104 +1,187 @@
1
1
  """
2
- Lambda@Edge Origin-Request Handler for IP-based Access Gating
2
+ Lambda@Edge function for IP-based access gating.
3
3
  Geek Cafe, LLC
4
4
  Maintainers: Eric Wilson
5
- """
6
5
 
7
- import ipaddress
6
+ Since Lambda@Edge does not support environment variables, configuration
7
+ is fetched from SSM Parameter Store at runtime (with caching).
8
+ """
8
9
  import json
9
- import os
10
+ import ipaddress
11
+ import boto3
12
+ from functools import lru_cache
10
13
 
14
+ # SSM client - will be created in the region where the function executes
15
+ ssm = None
11
16
 
12
- def lambda_handler(event, context):
17
+ @lru_cache(maxsize=128)
18
+ def get_ssm_parameter(parameter_name: str, region: str = 'us-east-1') -> str:
13
19
  """
14
- Lambda@Edge origin-request handler that implements IP-based gating
15
- with maintenance site fallback.
20
+ Fetch SSM parameter with caching.
21
+ Lambda@Edge cannot use environment variables, so we fetch from SSM.
16
22
 
17
- Features:
18
- - Inject X-Viewer-IP header for origin visibility
19
- - Check viewer IP against allowlist when gate is enabled
20
- - Rewrite blocked IPs to maintenance CloudFront distribution, and serve up maintenance site
21
- - Toggle via GATE_ENABLED environment variable
22
- """
23
+ Args:
24
+ parameter_name: Name of the SSM parameter
25
+ region: AWS region (default us-east-1)
23
26
 
24
- # Extract request from CloudFront event
25
- request = event['Records'][0]['cf']['request']
26
- client_ip = request['clientIp']
27
+ Returns:
28
+ Parameter value
29
+ """
30
+ global ssm
31
+ if ssm is None:
32
+ ssm = boto3.client('ssm', region_name=region)
27
33
 
28
- # Configuration from environment variables
29
- gate_enabled = os.environ.get('GATE_ENABLED', 'false').lower() == 'true'
30
- allow_cidrs_str = os.environ.get('ALLOW_CIDRS', '')
31
- maint_cf_host = os.environ.get('MAINT_CF_HOST', '')
34
+ try:
35
+ response = ssm.get_parameter(Name=parameter_name, WithDecryption=False)
36
+ return response['Parameter']['Value']
37
+ except Exception as e:
38
+ print(f"Error fetching SSM parameter {parameter_name}: {str(e)}")
39
+ raise
40
+
41
+
42
+ def get_client_ip(request):
43
+ """Extract client IP from CloudFront request."""
44
+ if 'clientIp' in request:
45
+ return request['clientIp']
32
46
 
33
- # Parse allowed CIDRs
34
- allow_cidrs = [cidr.strip() for cidr in allow_cidrs_str.split(',') if cidr.strip()]
47
+ # Fallback to headers
48
+ headers = request.get('headers', {})
49
+ if 'x-forwarded-for' in headers:
50
+ xff = headers['x-forwarded-for'][0]['value']
51
+ return xff.split(',')[0].strip()
35
52
 
36
- # Always inject viewer IP header
37
- if 'headers' not in request:
38
- request['headers'] = {}
53
+ return None
54
+
55
+
56
+ def is_ip_allowed(client_ip: str, allowed_cidrs: list) -> bool:
57
+ """
58
+ Check if client IP is in any of the allowed CIDR ranges.
39
59
 
40
- request['headers']['x-viewer-ip'] = [{
41
- 'key': 'X-Viewer-IP',
42
- 'value': client_ip
43
- }]
60
+ Args:
61
+ client_ip: Client IP address
62
+ allowed_cidrs: List of CIDR ranges (e.g., ['10.0.0.0/8', '192.168.1.0/24'])
44
63
 
45
- # If gate is disabled, pass through to origin
46
- if not gate_enabled:
47
- return request
64
+ Returns:
65
+ True if IP is allowed, False otherwise
66
+ """
67
+ if not client_ip:
68
+ return False
48
69
 
49
- # Check if IP is in allowlist
50
- ip_allowed = False
51
70
  try:
52
71
  client_ip_obj = ipaddress.ip_address(client_ip)
53
- for cidr in allow_cidrs:
54
- try:
55
- network = ipaddress.ip_network(cidr, strict=False)
56
- if client_ip_obj in network:
57
- ip_allowed = True
58
- break
59
- except (ValueError, ipaddress.AddressValueError):
60
- # Invalid CIDR, skip
61
- continue
62
- except (ValueError, ipaddress.AddressValueError):
63
- # Invalid client IP, block by default
64
- ip_allowed = False
72
+ except ValueError as e:
73
+ print(f"Invalid client IP address: {e}")
74
+ return False
65
75
 
66
- # If IP is allowed, pass through to origin
67
- if ip_allowed:
68
- return request
76
+ # Check each CIDR individually, skipping invalid ones
77
+ for cidr in allowed_cidrs:
78
+ try:
79
+ network = ipaddress.ip_network(cidr.strip(), strict=False)
80
+ if client_ip_obj in network:
81
+ return True
82
+ except ValueError as e:
83
+ # Invalid CIDR, log and continue checking others
84
+ print(f"Invalid CIDR '{cidr}': {e}")
85
+ continue
69
86
 
70
- # IP not allowed - redirect to maintenance site
71
- if not maint_cf_host:
72
- # Safety: if maintenance host not configured, pass through with warning
73
- # In production, you might want to return a fixed error response instead
74
- return request
87
+ return False
88
+
89
+
90
+ def lambda_handler(event, context):
91
+ """
92
+ Lambda@Edge function for IP-based gating.
75
93
 
76
- # Rewrite origin to maintenance CloudFront distribution
77
- request['origin'] = {
78
- 'custom': {
79
- 'domainName': maint_cf_host,
80
- 'port': 443,
81
- 'protocol': 'https',
82
- 'path': '',
83
- 'sslProtocols': ['TLSv1.2'],
84
- 'readTimeout': 30,
85
- 'keepaliveTimeout': 5,
86
- 'customHeaders': {}
87
- }
88
- }
94
+ Configuration (fetched from SSM Parameter Store):
95
+ - GATE_ENABLED: Whether IP gating is enabled (true/false)
96
+ - ALLOW_CIDRS: Comma-separated list of allowed CIDR ranges
97
+ - MAINT_CF_HOST: CloudFront domain for maintenance/lockout page
89
98
 
90
- # Update Host header to match new origin
91
- request['headers']['host'] = [{
92
- 'key': 'Host',
93
- 'value': maint_cf_host
94
- }]
99
+ Runtime configuration is bundled in runtime_config.json by CDK.
100
+ SSM parameter paths are auto-generated by CDK as:
101
+ /{environment}/{full-function-name}/{env-var-name-kebab-case}
102
+ """
103
+ request = event['Records'][0]['cf']['request']
104
+
105
+ # Load runtime configuration bundled by CDK
106
+ # This file is created during deployment and contains environment, function name, etc.
107
+ try:
108
+ with open('runtime_config.json', 'r') as f:
109
+ runtime_config = json.load(f)
110
+
111
+ env = runtime_config.get('environment', 'dev')
112
+ function_base_name = runtime_config.get('function_name', 'ip-gate')
113
+
114
+ print(f"Runtime config loaded: environment={env}, function_name={function_base_name}")
115
+ except FileNotFoundError:
116
+ # Fallback: extract from Lambda context (less reliable)
117
+ print("Warning: runtime_config.json not found, falling back to function name parsing")
118
+ function_full_name = context.function_name
119
+
120
+ # Parse environment from function name as fallback
121
+ parts = function_full_name.split('-')
122
+ common_envs = ['dev', 'prod', 'staging', 'test', 'qa', 'uat']
123
+ env = 'dev'
124
+
125
+ for part in parts:
126
+ if part in common_envs:
127
+ env = part
128
+ break
129
+
130
+ function_base_name = 'ip-gate'
131
+ print(f"Fallback: environment={env}, function_name={function_base_name}")
95
132
 
96
- # Normalize URI - redirect directory requests to index.html
97
- uri = request.get('uri', '/')
98
- if uri.endswith('/'):
99
- request['uri'] = uri + 'index.html'
100
- elif '.' not in uri.split('/')[-1]:
101
- # No file extension, likely a directory
102
- request['uri'] = uri + '/index.html'
133
+ # Full function name for SSM paths
134
+ function_name = context.function_name
135
+ print(f"Lambda function ARN: {context.invoked_function_arn}")
103
136
 
104
- return request
137
+ try:
138
+ # Fetch configuration from SSM Parameter Store
139
+ # Auto-generated paths: /{env}/{function-name}/{key}
140
+ gate_enabled = get_ssm_parameter(f'/{env}/{function_name}/gate-enabled', 'us-east-1')
141
+
142
+ # If gating is disabled, allow all traffic
143
+ if gate_enabled.lower() not in ('true', '1', 'yes'):
144
+ print(f"IP gating is disabled (GATE_ENABLED={gate_enabled})")
145
+ return request
146
+
147
+ # Get allowed CIDRs and maintenance host
148
+ allow_cidrs_str = get_ssm_parameter(f'/{env}/{function_name}/allow-cidrs', 'us-east-1')
149
+ maint_cf_host = get_ssm_parameter(f'/{env}/{function_name}/maint-cf-host', 'us-east-1')
150
+
151
+ # Parse allowed CIDRs
152
+ allowed_cidrs = [cidr.strip() for cidr in allow_cidrs_str.split(',') if cidr.strip()]
153
+
154
+ # Get client IP
155
+ client_ip = get_client_ip(request)
156
+ print(f"Client IP: {client_ip}")
157
+
158
+ # Check if IP is allowed
159
+ if is_ip_allowed(client_ip, allowed_cidrs):
160
+ print(f"IP {client_ip} is allowed")
161
+ return request
162
+
163
+ # IP not allowed - redirect to maintenance page
164
+ print(f"IP {client_ip} is NOT allowed, redirecting to {maint_cf_host}")
165
+
166
+ response = {
167
+ 'status': '302',
168
+ 'statusDescription': 'Found',
169
+ 'headers': {
170
+ 'location': [{
171
+ 'key': 'Location',
172
+ 'value': f'https://{maint_cf_host}'
173
+ }],
174
+ 'cache-control': [{
175
+ 'key': 'Cache-Control',
176
+ 'value': 'no-cache, no-store, must-revalidate'
177
+ }]
178
+ }
179
+ }
180
+
181
+ return response
182
+
183
+ except Exception as e:
184
+ print(f"Error in IP gating function: {str(e)}")
185
+ # On error, allow the request to proceed (fail open)
186
+ # Change this to fail closed if preferred
187
+ return request
@@ -9,6 +9,9 @@ MIT License. See Project Root for the license information.
9
9
  from typing import Optional, Dict
10
10
  from pathlib import Path
11
11
  import json
12
+ import tempfile
13
+ import shutil
14
+ import importlib.resources
12
15
 
13
16
  import aws_cdk as cdk
14
17
  from aws_cdk import aws_lambda as _lambda
@@ -126,11 +129,38 @@ class LambdaEdgeStack(IStack, EnhancedSsmParameterMixin):
126
129
  def _create_lambda_function(self, function_name: str) -> None:
127
130
  """Create the Lambda function"""
128
131
 
129
- # Resolve code path (relative to runtime directory or absolute)
130
- code_path = Path(self.edge_config.code_path)
131
- if not code_path.is_absolute():
132
- # Assume relative to the project root
133
- code_path = Path.cwd() / code_path
132
+ # Resolve code path - support package references (e.g., "cdk_factory:lambdas/edge/ip_gate")
133
+ code_path_str = self.edge_config.code_path
134
+
135
+ if ':' in code_path_str:
136
+ # Package reference format: "package_name:path/within/package"
137
+ package_name, package_path = code_path_str.split(':', 1)
138
+ logger.info(f"Resolving package reference: {package_name}:{package_path}")
139
+
140
+ try:
141
+ # Get the package's installed location
142
+ if hasattr(importlib.resources, 'files'):
143
+ # Python 3.9+
144
+ package_root = importlib.resources.files(package_name)
145
+ code_path = Path(str(package_root / package_path))
146
+ else:
147
+ # Fallback for older Python
148
+ import pkg_resources
149
+ package_root = pkg_resources.resource_filename(package_name, '')
150
+ code_path = Path(package_root) / package_path
151
+
152
+ logger.info(f"Resolved package path to: {code_path}")
153
+ except Exception as e:
154
+ raise FileNotFoundError(
155
+ f"Could not resolve package reference '{code_path_str}': {e}\n"
156
+ f"Make sure package '{package_name}' is installed."
157
+ )
158
+ else:
159
+ # Regular file path
160
+ code_path = Path(code_path_str)
161
+ if not code_path.is_absolute():
162
+ # Assume relative to the project root
163
+ code_path = Path.cwd() / code_path
134
164
 
135
165
  if not code_path.exists():
136
166
  raise FileNotFoundError(
@@ -140,6 +170,15 @@ class LambdaEdgeStack(IStack, EnhancedSsmParameterMixin):
140
170
 
141
171
  logger.info(f"Loading Lambda code from: {code_path}")
142
172
 
173
+ # Create isolated temp directory for this function instance
174
+ # This prevents conflicts when multiple functions use the same handler code
175
+ temp_code_dir = Path(tempfile.mkdtemp(prefix=f"{function_name.replace('/', '-')}-"))
176
+ logger.info(f"Creating isolated code directory at: {temp_code_dir}")
177
+
178
+ # Copy source code to temp directory
179
+ shutil.copytree(code_path, temp_code_dir, dirs_exist_ok=True)
180
+ logger.info(f"Copied code from {code_path} to {temp_code_dir}")
181
+
143
182
  # Create runtime configuration file for Lambda@Edge
144
183
  # Since Lambda@Edge doesn't support environment variables, we bundle a config file
145
184
  runtime_config = {
@@ -148,7 +187,7 @@ class LambdaEdgeStack(IStack, EnhancedSsmParameterMixin):
148
187
  'region': self.deployment.region
149
188
  }
150
189
 
151
- runtime_config_path = code_path / 'runtime_config.json'
190
+ runtime_config_path = temp_code_dir / 'runtime_config.json'
152
191
  logger.info(f"Creating runtime config at: {runtime_config_path}")
153
192
 
154
193
  with open(runtime_config_path, 'w') as f:
@@ -156,6 +195,9 @@ class LambdaEdgeStack(IStack, EnhancedSsmParameterMixin):
156
195
 
157
196
  logger.info(f"Runtime config: {runtime_config}")
158
197
 
198
+ # Use the temp directory for the Lambda code asset
199
+ code_path = temp_code_dir
200
+
159
201
  # Map runtime string to CDK Runtime
160
202
  runtime_map = {
161
203
  "python3.11": _lambda.Runtime.PYTHON_3_11,
cdk_factory/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.13.0"
1
+ __version__ = "0.13.2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cdk_factory
3
- Version: 0.13.0
3
+ Version: 0.13.2
4
4
  Summary: CDK Factory. A QuickStarter and best practices setup for CDK projects
5
5
  Author-email: Eric Wilson <eric.wilson@geekcafe.com>
6
6
  License: MIT License
@@ -2,7 +2,7 @@ cdk_factory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  cdk_factory/app.py,sha256=RnX0-pwdTAPAdKJK_j13Zl8anf9zYKBwboR0KA8K8xM,10346
3
3
  cdk_factory/cdk.json,sha256=SKZKhJ2PBpFH78j-F8S3VDYW-lf76--Q2I3ON-ZIQfw,3106
4
4
  cdk_factory/cli.py,sha256=FGbCTS5dYCNsfp-etshzvFlGDCjC28r6rtzYbe7KoHI,6407
5
- cdk_factory/version.py,sha256=DgpLNbv0e1LIEOOe54Db8_390i9pelMEFEnsBsNmyhA,23
5
+ cdk_factory/version.py,sha256=blu6md2c3Nnj5gDBi8U36sYO3k8HcND8s7UoQBjfn3g,23
6
6
  cdk_factory/builds/README.md,sha256=9BBWd7bXpyKdMU_g2UljhQwrC9i5O_Tvkb6oPvndoZk,90
7
7
  cdk_factory/commands/command_loader.py,sha256=QbLquuP_AdxtlxlDy-2IWCQ6D-7qa58aphnDPtp_uTs,3744
8
8
  cdk_factory/configurations/base_config.py,sha256=JKjhNsy0RCUZy1s8n5D_aXXI-upR9izaLtCTfKYiV9k,9624
@@ -66,7 +66,7 @@ cdk_factory/interfaces/istack.py,sha256=bhTBs-o9FgKwvJMSuwxjUV6D3nUlvZHVzfm27jP9
66
66
  cdk_factory/interfaces/live_ssm_resolver.py,sha256=3FIr9a02SXqZmbFs3RT0WxczWEQR_CF7QSt7kWbDrVE,8163
67
67
  cdk_factory/interfaces/ssm_parameter_mixin.py,sha256=uA2j8HmAOpuEA9ynRj51s0WjUHMVLsbLQN-QS9NKyHA,12089
68
68
  cdk_factory/lambdas/health_handler.py,sha256=dd40ykKMxWCFEIyp2ZdQvAGNjw_ylI9CSm1N24Hp2ME,196
69
- cdk_factory/lambdas/edge/ip_gate/handler.py,sha256=MPSAOQGYVMSALdH6f9QTXqMSKKJ1tva-yidtaDTbXsg,3241
69
+ cdk_factory/lambdas/edge/ip_gate/handler.py,sha256=NCa16_B66zIczPb9whf_CaspnetuTYKTFpIX6Z7Wqjw,6393
70
70
  cdk_factory/pipeline/path_utils.py,sha256=fvWdrcb4onmpIu1APkHLhXg8zWfK74HcW3Ra2ynxfXM,2586
71
71
  cdk_factory/pipeline/pipeline_factory.py,sha256=rvtkdlTPJG477nTVRN8S2ksWt4bwpd9eVLFd9WO02pM,17248
72
72
  cdk_factory/pipeline/stage.py,sha256=Be7ExMB9A-linRM18IQDOzQ-cP_I2_ThRNzlT4FIrUg,437
@@ -95,7 +95,7 @@ cdk_factory/stack_library/ecr/ecr_stack.py,sha256=1xA68sxFVyqreYjXrP_7U9I8RF9RtF
95
95
  cdk_factory/stack_library/ecs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
96
  cdk_factory/stack_library/ecs/ecs_service_stack.py,sha256=zuGdZEP5KmeVDTJb-H47LYhvs-85-Fi4Xb78nsA-lF4,24685
97
97
  cdk_factory/stack_library/lambda_edge/__init__.py,sha256=ByBJ_CWdc4UtTmFBZH-6pzBMNkjkdtE65AmnB0Fs6lM,156
98
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py,sha256=WDLoARmn-ehGnfm8m9Kt88Mp1GQ6ZdqkQaDWE65hXD4,13478
98
+ cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py,sha256=fCnS_WFUwe9gyvsqrevevrDcUqZpZgnwM8v9tQVxFzk,15491
99
99
  cdk_factory/stack_library/load_balancer/__init__.py,sha256=wZpKw2OecLJGdF5mPayCYAEhu2H3c2gJFFIxwXftGDU,52
100
100
  cdk_factory/stack_library/load_balancer/load_balancer_stack.py,sha256=t5JUe5lMUbQCRFZR08k8nO-g-53yWY8gKB9v8ZnedBs,24391
101
101
  cdk_factory/stack_library/monitoring/__init__.py,sha256=k1G_KDx47Aw0UugaL99PN_TKlyLK4nkJVApCaAK7GJg,153
@@ -129,8 +129,8 @@ cdk_factory/utilities/lambda_function_utilities.py,sha256=S1GvBsY_q2cyUiaud3HORJ
129
129
  cdk_factory/utilities/os_execute.py,sha256=5Op0LY_8Y-pUm04y1k8MTpNrmQvcLmQHPQITEP7EuSU,1019
130
130
  cdk_factory/utils/api_gateway_utilities.py,sha256=If7Xu5s_UxmuV-kL3JkXxPLBdSVUKoLtohm0IUFoiV8,4378
131
131
  cdk_factory/workload/workload_factory.py,sha256=mM8GU_5mKq_0OyK060T3JrUSUiGAcKf0eqNlT9mfaws,6028
132
- cdk_factory-0.13.0.dist-info/METADATA,sha256=u2EL937XENu8doeidJ_nCCJIY0h18_KGWwTk2nQ7ADQ,2451
133
- cdk_factory-0.13.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
134
- cdk_factory-0.13.0.dist-info/entry_points.txt,sha256=S1DPe0ORcdiwEALMN_WIo3UQrW_g4YdQCLEsc_b0Swg,53
135
- cdk_factory-0.13.0.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
136
- cdk_factory-0.13.0.dist-info/RECORD,,
132
+ cdk_factory-0.13.2.dist-info/METADATA,sha256=LyiWwDWEaFep_Koxoo46zUxv3Lza_88v4T45EK7zNeg,2451
133
+ cdk_factory-0.13.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
134
+ cdk_factory-0.13.2.dist-info/entry_points.txt,sha256=S1DPe0ORcdiwEALMN_WIo3UQrW_g4YdQCLEsc_b0Swg,53
135
+ cdk_factory-0.13.2.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
136
+ cdk_factory-0.13.2.dist-info/RECORD,,