cdk-factory 0.7.26__py3-none-any.whl → 0.7.27__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.
@@ -72,7 +72,7 @@ class ApiGatewayConfigRouteConfig:
72
72
  @property
73
73
  def allow_public_override(self) -> bool:
74
74
  """Whether to allow public access when Cognito is available"""
75
- return self._config.get("allow_public_override", False)
75
+ return str(self._config.get("allow_public_override", False)).lower() == "true"
76
76
 
77
77
  @property
78
78
  def dictionary(self) -> Dict[str, Any]:
@@ -350,28 +350,30 @@ class ApiGatewayStack(IStack, EnhancedSsmParameterMixin):
350
350
  def _validate_authorization_configuration(self, route, has_cognito_authorizer):
351
351
  """
352
352
  Validate authorization configuration for security and clarity.
353
-
353
+
354
354
  This method implements 'secure by default' with explicit overrides:
355
355
  - If Cognito is available and route wants NONE auth, requires explicit override
356
356
  - If Cognito is not available and route wants COGNITO auth, raises error
357
357
  - Provides verbose warnings for monitoring and security awareness
358
-
358
+
359
359
  Args:
360
360
  route (dict): Route configuration
361
361
  has_cognito_authorizer (bool): Whether a Cognito authorizer is configured
362
-
362
+
363
363
  Raises:
364
364
  ValueError: When there are security conflicts without explicit overrides
365
365
  """
366
366
  import logging
367
-
368
- auth_type = route.get("authorization_type", "COGNITO")
369
- explicit_override = route.get("allow_public_override", False)
367
+
368
+ auth_type = str(route.get("authorization_type", "COGNITO")).upper()
369
+ explicit_override = (
370
+ str(route.get("allow_public_override", False)).lower() == "true"
371
+ )
370
372
  route_path = route.get("path", "unknown")
371
373
  method = route.get("method", "unknown")
372
-
374
+
373
375
  logger = logging.getLogger(__name__)
374
-
376
+
375
377
  # Case 1: Cognito available + NONE requested + No explicit override = ERROR
376
378
  if has_cognito_authorizer and auth_type == "NONE" and not explicit_override:
377
379
  error_msg = (
@@ -383,11 +385,12 @@ class ApiGatewayStack(IStack, EnhancedSsmParameterMixin):
383
385
  f" 1. Remove Cognito configuration if you want public access\n"
384
386
  f" 2. Add 'allow_public_override': true to explicitly allow public access\n"
385
387
  f" 3. Remove 'authorization_type': 'NONE' to use secure Cognito auth\n\n"
386
- f"🔒 This prevents accidental public endpoints when authentication is available."
388
+ f"🔒 This prevents accidental public endpoints when authentication is available.\n\n"
389
+ f"👉 ApiGatewayStack documentation for more details: https://github.com/your-repo/api-gateway-stack"
387
390
  )
388
391
  raise ValueError(error_msg)
389
-
390
- # Case 2: No Cognito + COGNITO explicitly requested = ERROR
392
+
393
+ # Case 2: No Cognito + COGNITO explicitly requested = ERROR
391
394
  # Only error if COGNITO was explicitly requested, not if it's the default
392
395
  if not has_cognito_authorizer and route.get("authorization_type") == "COGNITO":
393
396
  error_msg = (
@@ -401,7 +404,7 @@ class ApiGatewayStack(IStack, EnhancedSsmParameterMixin):
401
404
  f" 4. Remove explicit authorization_type to use default behavior"
402
405
  )
403
406
  raise ValueError(error_msg)
404
-
407
+
405
408
  # Case 3: Cognito available + NONE requested + Explicit override = WARN
406
409
  if has_cognito_authorizer and auth_type == "NONE" and explicit_override:
407
410
  warning_msg = (
@@ -411,10 +414,10 @@ class ApiGatewayStack(IStack, EnhancedSsmParameterMixin):
411
414
  f" 📊 Consider monitoring this endpoint for unexpected usage patterns\n"
412
415
  f" 🔍 Review periodically: Should this endpoint be secured?"
413
416
  )
414
-
417
+
415
418
  # Print to console during deployment for visibility
416
419
  print(warning_msg)
417
-
420
+
418
421
  # Structured logging for monitoring and metrics
419
422
  logger.warning(
420
423
  "Public endpoint configured with Cognito available",
@@ -426,10 +429,10 @@ class ApiGatewayStack(IStack, EnhancedSsmParameterMixin):
426
429
  "authorization_type": "NONE",
427
430
  "metric_name": "public_endpoint_with_cognito",
428
431
  "security_decision": "intentional_public",
429
- "recommendation": "review_periodically"
430
- }
432
+ "recommendation": "review_periodically",
433
+ },
431
434
  )
432
-
435
+
433
436
  # Case 4: No Cognito + NONE = INFO (expected for public-only APIs)
434
437
  if not has_cognito_authorizer and auth_type == "NONE":
435
438
  logger.info(
@@ -439,8 +442,8 @@ class ApiGatewayStack(IStack, EnhancedSsmParameterMixin):
439
442
  "method": method,
440
443
  "authorization_type": "NONE",
441
444
  "cognito_available": False,
442
- "security_decision": "public_only_api"
443
- }
445
+ "security_decision": "public_only_api",
446
+ },
444
447
  )
445
448
 
446
449
  def _setup_lambda_integration(
@@ -448,23 +451,27 @@ class ApiGatewayStack(IStack, EnhancedSsmParameterMixin):
448
451
  ):
449
452
  """Setup Lambda integration for a route"""
450
453
  import logging
451
-
454
+
452
455
  route_path = route["path"]
453
456
  # Secure by default: require Cognito authorization unless explicitly set to NONE
454
457
  authorization_type = route.get("authorization_type", "COGNITO")
455
-
458
+
456
459
  # If no Cognito authorizer available and default COGNITO, fall back to NONE
457
- if not authorizer and authorization_type == "COGNITO" and "authorization_type" not in route:
460
+ if (
461
+ not authorizer
462
+ and authorization_type == "COGNITO"
463
+ and "authorization_type" not in route
464
+ ):
458
465
  authorization_type = "NONE"
459
466
  logger = logging.getLogger(__name__)
460
467
  logger.info(
461
468
  f"No Cognito authorizer available for route {route_path} ({route.get('method', 'unknown')}), "
462
469
  f"defaulting to public access (NONE authorization)"
463
470
  )
464
-
471
+
465
472
  # Validate authorization configuration for security
466
473
  self._validate_authorization_configuration(route, authorizer is not None)
467
-
474
+
468
475
  # If set to NONE (explicitly or by fallback), skip authorization
469
476
  if authorization_type == "NONE":
470
477
  authorizer = None
@@ -502,20 +509,24 @@ class ApiGatewayStack(IStack, EnhancedSsmParameterMixin):
502
509
  ):
503
510
  """Setup fallback Lambda integration for routes without src"""
504
511
  import logging
505
-
512
+
506
513
  route_path = route["path"]
507
514
  # Secure by default: require Cognito authorization unless explicitly set to NONE
508
515
  authorization_type = route.get("authorization_type", "COGNITO")
509
-
516
+
510
517
  # If no Cognito authorizer available and default COGNITO, fall back to NONE
511
- if not authorizer and authorization_type == "COGNITO" and "authorization_type" not in route:
518
+ if (
519
+ not authorizer
520
+ and authorization_type == "COGNITO"
521
+ and "authorization_type" not in route
522
+ ):
512
523
  authorization_type = "NONE"
513
524
  logger = logging.getLogger(__name__)
514
525
  logger.info(
515
526
  f"No Cognito authorizer available for route {route_path} ({route.get('method', 'unknown')}), "
516
527
  f"defaulting to public access (NONE authorization)"
517
528
  )
518
-
529
+
519
530
  # Validate authorization configuration for security
520
531
  self._validate_authorization_configuration(route, authorizer is not None)
521
532
 
@@ -56,10 +56,13 @@ class ApiGatewayIntegrationUtility:
56
56
 
57
57
  # Validate authorization configuration for security
58
58
  has_cognito_authorizer = (
59
- self.authorizer is not None or
60
- self._get_existing_authorizer_id_with_ssm_fallback(api_config, stack_config) is not None
59
+ self.authorizer is not None
60
+ or self._get_existing_authorizer_id_with_ssm_fallback(
61
+ api_config, stack_config
62
+ )
63
+ is not None
61
64
  )
62
-
65
+
63
66
  # Apply enhanced authorization validation and fallback logic
64
67
  api_config = self._validate_and_adjust_authorization_configuration(
65
68
  api_config, has_cognito_authorizer
@@ -79,7 +82,9 @@ class ApiGatewayIntegrationUtility:
79
82
  )
80
83
 
81
84
  # Add method to API Gateway
82
- resource = self.get_or_create_resource(api_gateway, api_config.routes, stack_config)
85
+ resource = self.get_or_create_resource(
86
+ api_gateway, api_config.routes, stack_config
87
+ )
83
88
 
84
89
  # Handle existing authorizer ID using L1 constructs
85
90
  if self._get_existing_authorizer_id_with_ssm_fallback(api_config, stack_config):
@@ -101,7 +106,7 @@ class ApiGatewayIntegrationUtility:
101
106
  # Use configured authorization type
102
107
  auth_type = apigateway.AuthorizationType[api_config.authorization_type]
103
108
  authorizer_to_use = None
104
-
109
+
105
110
  method = None
106
111
  try:
107
112
  method = resource.add_method(
@@ -623,9 +628,11 @@ class ApiGatewayIntegrationUtility:
623
628
  if stack_config:
624
629
  api_gateway_config = stack_config.dictionary.get("api_gateway", {})
625
630
  existing_resources = api_gateway_config.get("existing_resources", {})
626
-
631
+
627
632
  if existing_resources:
628
- return self._create_resource_with_imports(api_gateway, route_path, existing_resources)
633
+ return self._create_resource_with_imports(
634
+ api_gateway, route_path, existing_resources
635
+ )
629
636
 
630
637
  # Use the built-in resource_for_path method which handles existing resources correctly
631
638
  try:
@@ -682,27 +689,29 @@ class ApiGatewayIntegrationUtility:
682
689
  ) -> apigateway.Resource:
683
690
  """Create resource path using existing resource imports to avoid conflicts"""
684
691
  from aws_cdk import aws_apigateway as apigateway
685
-
692
+
686
693
  # Remove leading slash and split path
687
694
  path_parts = route_path.lstrip("/").split("/")
688
695
  current_resource = api_gateway.root
689
696
  current_path = ""
690
-
697
+
691
698
  # Navigate through path parts, importing existing resources where configured
692
699
  for i, part in enumerate(path_parts):
693
700
  if not part: # Skip empty parts
694
701
  continue
695
-
696
- current_path = "/" + "/".join(path_parts[:i+1])
697
-
702
+
703
+ current_path = "/" + "/".join(path_parts[: i + 1])
704
+
698
705
  # Check if this path segment should be imported from existing resources
699
706
  if current_path in existing_resources:
700
707
  resource_config = existing_resources[current_path]
701
708
  resource_id = resource_config.get("resource_id")
702
-
709
+
703
710
  if resource_id:
704
- logger.info(f"Importing existing resource for path {current_path} with ID: {resource_id}")
705
-
711
+ logger.info(
712
+ f"Importing existing resource for path {current_path} with ID: {resource_id}"
713
+ )
714
+
706
715
  # Import the existing resource using L1 constructs
707
716
  current_resource = self._import_existing_resource(
708
717
  api_gateway, current_resource, part, resource_id, current_path
@@ -713,33 +722,39 @@ class ApiGatewayIntegrationUtility:
713
722
  else:
714
723
  # Create normally for non-imported paths
715
724
  current_resource = self._add_resource_safely(current_resource, part)
716
-
725
+
717
726
  return current_resource
718
-
727
+
719
728
  def _import_existing_resource(
720
- self, api_gateway: apigateway.RestApi, parent_resource: apigateway.Resource,
721
- path_part: str, resource_id: str, full_path: str
729
+ self,
730
+ api_gateway: apigateway.RestApi,
731
+ parent_resource: apigateway.Resource,
732
+ path_part: str,
733
+ resource_id: str,
734
+ full_path: str,
722
735
  ) -> apigateway.Resource:
723
736
  """Import an existing API Gateway resource by ID"""
724
737
  from aws_cdk import aws_apigateway as apigateway
725
-
738
+
726
739
  try:
727
740
  # Use CfnResource to reference existing resource
728
741
  # This creates a reference without trying to create the resource
729
742
  imported_resource = apigateway.Resource.from_resource_id(
730
- self.scope,
731
- f"imported-resource-{hash(full_path) % 10000}",
732
- resource_id
743
+ self.scope, f"imported-resource-{hash(full_path) % 10000}", resource_id
744
+ )
745
+
746
+ logger.info(
747
+ f"Successfully imported existing resource: {path_part} (ID: {resource_id})"
733
748
  )
734
-
735
- logger.info(f"Successfully imported existing resource: {path_part} (ID: {resource_id})")
736
749
  return imported_resource
737
-
750
+
738
751
  except Exception as e:
739
- logger.warning(f"Failed to import resource {path_part} with ID {resource_id}: {e}")
752
+ logger.warning(
753
+ f"Failed to import resource {path_part} with ID {resource_id}: {e}"
754
+ )
740
755
  # Fallback to normal creation
741
756
  return self._add_resource_safely(parent_resource, path_part)
742
-
757
+
743
758
  def _add_resource_safely(
744
759
  self, parent_resource: apigateway.Resource, path_part: str
745
760
  ) -> apigateway.Resource:
@@ -747,9 +762,13 @@ class ApiGatewayIntegrationUtility:
747
762
  try:
748
763
  return parent_resource.add_resource(path_part)
749
764
  except Exception as e:
750
- if "AlreadyExists" in str(e) or "same parent already has this name" in str(e):
751
- logger.warning(f"Resource {path_part} already exists, attempting to find existing resource")
752
-
765
+ if "AlreadyExists" in str(e) or "same parent already has this name" in str(
766
+ e
767
+ ):
768
+ logger.warning(
769
+ f"Resource {path_part} already exists, attempting to find existing resource"
770
+ )
771
+
753
772
  # Try to find the existing resource in children
754
773
  for child in parent_resource.node.children:
755
774
  if (
@@ -758,7 +777,7 @@ class ApiGatewayIntegrationUtility:
758
777
  ):
759
778
  logger.info(f"Found existing resource: {path_part}")
760
779
  return child
761
-
780
+
762
781
  # If not found in children, re-raise the error
763
782
  logger.error(f"Could not find or create resource: {path_part}")
764
783
  raise e
@@ -837,7 +856,7 @@ class ApiGatewayIntegrationUtility:
837
856
  # Try enhanced SSM parameter lookup with auto-discovery
838
857
  api_gateway_config = stack_config.dictionary.get("api_gateway", {})
839
858
  ssm_config = api_gateway_config.get("ssm", {})
840
-
859
+
841
860
  if ssm_config.get("enabled", False):
842
861
  try:
843
862
  from cdk_factory.interfaces.enhanced_ssm_parameter_mixin import (
@@ -859,26 +878,34 @@ class ApiGatewayIntegrationUtility:
859
878
  imports_config = ssm_config.get("imports", {})
860
879
  if "authorizer_id" in imports_config:
861
880
  import_value = imports_config["authorizer_id"]
862
-
881
+
863
882
  if import_value == "auto":
864
883
  logger.info("Using auto-import for authorizer ID")
865
884
  imported_values = ssm_mixin.auto_import_resources()
866
885
  authorizer_id = imported_values.get("authorizer_id")
867
886
  if authorizer_id:
868
- logger.info(f"Found authorizer ID via auto-import: {authorizer_id}")
887
+ logger.info(
888
+ f"Found authorizer ID via auto-import: {authorizer_id}"
889
+ )
869
890
  return authorizer_id
870
891
  else:
871
892
  # Use direct parameter import for specific SSM path
872
- logger.info(f"Looking up authorizer ID from SSM parameter: {import_value}")
893
+ logger.info(
894
+ f"Looking up authorizer ID from SSM parameter: {import_value}"
895
+ )
873
896
  authorizer_id = ssm_mixin._import_enhanced_ssm_parameter(
874
897
  import_value, "authorizer_id"
875
898
  )
876
899
  if authorizer_id:
877
- logger.info(f"Found authorizer ID from SSM: {authorizer_id}")
900
+ logger.info(
901
+ f"Found authorizer ID from SSM: {authorizer_id}"
902
+ )
878
903
  return authorizer_id
879
904
 
880
905
  except Exception as e:
881
- logger.warning(f"Failed to retrieve authorizer ID via enhanced SSM: {e}")
906
+ logger.warning(
907
+ f"Failed to retrieve authorizer ID via enhanced SSM: {e}"
908
+ )
882
909
 
883
910
  # Fallback to traditional SSM parameter lookup
884
911
  authorizer_config = stack_config.dictionary.get("api_gateway", {}).get(
@@ -1041,7 +1068,9 @@ class ApiGatewayIntegrationUtility:
1041
1068
 
1042
1069
  return exported_params
1043
1070
 
1044
- def setup_route_cors(self, resource: apigateway.Resource, route_path: str, route: dict):
1071
+ def setup_route_cors(
1072
+ self, resource: apigateway.Resource, route_path: str, route: dict
1073
+ ):
1045
1074
  """Setup CORS for a route - centralized method for both API Gateway and Lambda stacks"""
1046
1075
  cors_cfg = route.get("cors")
1047
1076
  methods = cors_cfg.get("methods") if cors_cfg else None
@@ -1114,28 +1143,30 @@ class ApiGatewayIntegrationUtility:
1114
1143
  stack_config: StackConfig,
1115
1144
  api_config: Optional[ApiGatewayConfig] = None,
1116
1145
  construct_scope: Optional[Construct] = None,
1117
- counter: int = 1
1146
+ counter: int = 1,
1118
1147
  ) -> apigateway.Stage:
1119
1148
  """
1120
1149
  Create deployment and stage for API Gateway with all integrations.
1121
1150
  Consolidates logic from both API Gateway and Lambda stacks.
1122
1151
  """
1123
1152
  scope = construct_scope or self.scope
1124
-
1153
+
1125
1154
  # Determine stage name with fallback logic
1126
1155
  stage_name = self._get_stage_name(stack_config, api_config)
1127
-
1156
+
1128
1157
  # Check if using existing stage
1129
1158
  use_existing = self._should_use_existing_stage(stack_config)
1130
-
1131
- logger.info(f"Creating deployment for API Gateway with {len(integrations)} integrations")
1132
-
1159
+
1160
+ logger.info(
1161
+ f"Creating deployment for API Gateway with {len(integrations)} integrations"
1162
+ )
1163
+
1133
1164
  # Create deployment
1134
1165
  deployment_id = f"api-gateway-{counter}-deployment-final"
1135
1166
  if len(integrations) == 1 and integrations[0].get("function_name"):
1136
1167
  # Lambda stack deployment
1137
1168
  deployment_id = "api-gateway-deployment"
1138
-
1169
+
1139
1170
  deployment = apigateway.Deployment(
1140
1171
  scope,
1141
1172
  deployment_id,
@@ -1145,83 +1176,95 @@ class ApiGatewayIntegrationUtility:
1145
1176
  )
1146
1177
  # Add timestamp to deployment logical ID to prevent conflicts and force new deployment
1147
1178
  deployment.add_to_logical_id(datetime.now(UTC).isoformat())
1148
-
1179
+
1149
1180
  # Create stage if not using existing
1150
1181
  stage = None
1151
1182
  if not use_existing:
1152
- stage_options = self._create_stage_options(api_config) if api_config else None
1183
+ stage_options = (
1184
+ self._create_stage_options(api_config) if api_config else None
1185
+ )
1153
1186
  stage_id = f"{api_gateway.rest_api_name}-{stage_name}-stage"
1154
1187
  if len(integrations) == 1 and integrations[0].get("function_name"):
1155
1188
  # Lambda stack stage
1156
1189
  stage_id = f"{api_gateway.rest_api_name}-{stage_name}-stage-lambdas"
1157
-
1190
+
1158
1191
  stage_kwargs = {
1159
1192
  "deployment": deployment,
1160
1193
  "stage_name": stage_name,
1161
- "description": f"Stage {stage_name} with {len(integrations)} integrations"
1194
+ "description": f"Stage {stage_name} with {len(integrations)} integrations",
1162
1195
  }
1163
-
1196
+
1164
1197
  # Add stage options if available
1165
1198
  if stage_options:
1166
- stage_kwargs.update({
1167
- "access_log_destination": stage_options.access_log_destination,
1168
- "access_log_format": stage_options.access_log_format,
1169
- "logging_level": stage_options.logging_level,
1170
- "data_trace_enabled": stage_options.data_trace_enabled,
1171
- "metrics_enabled": stage_options.metrics_enabled,
1172
- "tracing_enabled": stage_options.tracing_enabled,
1173
- "throttling_rate_limit": stage_options.throttling_rate_limit,
1174
- "throttling_burst_limit": stage_options.throttling_burst_limit
1175
- })
1176
-
1199
+ stage_kwargs.update(
1200
+ {
1201
+ "access_log_destination": stage_options.access_log_destination,
1202
+ "access_log_format": stage_options.access_log_format,
1203
+ "logging_level": stage_options.logging_level,
1204
+ "data_trace_enabled": stage_options.data_trace_enabled,
1205
+ "metrics_enabled": stage_options.metrics_enabled,
1206
+ "tracing_enabled": stage_options.tracing_enabled,
1207
+ "throttling_rate_limit": stage_options.throttling_rate_limit,
1208
+ "throttling_burst_limit": stage_options.throttling_burst_limit,
1209
+ }
1210
+ )
1211
+
1177
1212
  stage = apigateway.Stage(scope, stage_id, **stage_kwargs)
1178
-
1179
- logger.info(f"Created deployment and stage '{stage_name}' for API Gateway: {api_gateway.rest_api_name}")
1180
- logger.info(f"Routes available at: https://{api_gateway.rest_api_id}.execute-api.{scope.region}.amazonaws.com/{stage_name}")
1181
-
1213
+
1214
+ logger.info(
1215
+ f"Created deployment and stage '{stage_name}' for API Gateway: {api_gateway.rest_api_name}"
1216
+ )
1217
+ logger.info(
1218
+ f"Routes available at: https://{api_gateway.rest_api_id}.execute-api.{scope.region}.amazonaws.com/{stage_name}"
1219
+ )
1220
+
1182
1221
  return stage
1183
-
1184
- def _get_stage_name(self, stack_config: StackConfig, api_config: Optional[ApiGatewayConfig] = None) -> str:
1222
+
1223
+ def _get_stage_name(
1224
+ self, stack_config: StackConfig, api_config: Optional[ApiGatewayConfig] = None
1225
+ ) -> str:
1185
1226
  """Get stage name with fallback logic from both stacks"""
1186
1227
  # Try Lambda stack config format first
1187
1228
  api_gateway_config = stack_config.dictionary.get("api_gateway", {})
1188
1229
  stage_name = api_gateway_config.get("stage", {}).get("name")
1189
-
1230
+
1190
1231
  if stage_name:
1191
1232
  return stage_name
1192
-
1233
+
1193
1234
  # Try API Gateway stack config format
1194
- if api_config and hasattr(api_config, 'stage_name') and api_config.stage_name:
1235
+ if api_config and hasattr(api_config, "stage_name") and api_config.stage_name:
1195
1236
  stage_name = api_config.stage_name
1196
1237
  else:
1197
1238
  # Fallback to legacy format
1198
1239
  stage_name = api_gateway_config.get("stage_name", "prod")
1199
-
1240
+
1200
1241
  # Handle special cases
1201
1242
  if stage_name is None:
1202
1243
  raise ValueError("Stage name is required in API Gateway config")
1203
-
1244
+
1204
1245
  if stage_name.lower() == "auto":
1205
1246
  try:
1206
1247
  stage_name = stack_config.name
1207
1248
  except Exception as e:
1208
1249
  raise ValueError("Stage name is required in API Gateway config") from e
1209
-
1250
+
1210
1251
  return stage_name
1211
-
1252
+
1212
1253
  def _should_use_existing_stage(self, stack_config: StackConfig) -> bool:
1213
1254
  """Check if should use existing stage"""
1214
1255
  api_gateway_config = stack_config.dictionary.get("api_gateway", {})
1215
1256
  use_existing = api_gateway_config.get("stage", {}).get("use_existing", False)
1216
1257
  return str(use_existing).lower() == "true"
1217
-
1218
- def _create_stage_options(self, api_config: ApiGatewayConfig) -> apigateway.StageOptions:
1258
+
1259
+ def _create_stage_options(
1260
+ self, api_config: ApiGatewayConfig
1261
+ ) -> apigateway.StageOptions:
1219
1262
  """Create stage options with full configuration"""
1220
1263
  log_group = self._setup_log_group()
1221
1264
  access_log_format = self._get_log_format()
1222
-
1265
+
1223
1266
  deploy_options = api_config.deploy_options or {}
1224
-
1267
+
1225
1268
  return apigateway.StageOptions(
1226
1269
  access_log_destination=apigateway.LogGroupLogDestination(log_group),
1227
1270
  access_log_format=access_log_format,
@@ -1230,32 +1273,32 @@ class ApiGatewayIntegrationUtility:
1230
1273
  metrics_enabled=deploy_options.get("metrics_enabled", False),
1231
1274
  tracing_enabled=deploy_options.get("tracing_enabled", True),
1232
1275
  throttling_rate_limit=deploy_options.get("throttling_rate_limit", 1000),
1233
- throttling_burst_limit=deploy_options.get("throttling_burst_limit", 2000)
1276
+ throttling_burst_limit=deploy_options.get("throttling_burst_limit", 2000),
1234
1277
  )
1235
-
1278
+
1236
1279
  def _setup_log_group(self) -> logs.LogGroup:
1237
1280
  """Setup CloudWatch log group for API Gateway"""
1238
1281
  if self._log_group:
1239
1282
  return self._log_group
1240
-
1283
+
1241
1284
  self._log_group = logs.LogGroup(
1242
1285
  self.scope,
1243
1286
  "ApiGatewayLogGroup",
1244
1287
  removal_policy=RemovalPolicy.DESTROY,
1245
1288
  retention=logs.RetentionDays.ONE_MONTH,
1246
1289
  )
1247
-
1290
+
1248
1291
  self._log_group.grant_write(iam.ServicePrincipal("apigateway.amazonaws.com"))
1249
1292
  log_role = self._setup_log_role()
1250
1293
  self._log_group.grant_write(log_role)
1251
-
1294
+
1252
1295
  return self._log_group
1253
-
1296
+
1254
1297
  def _setup_log_role(self) -> iam.Role:
1255
1298
  """Setup IAM role for API Gateway logging"""
1256
1299
  if self._log_role:
1257
1300
  return self._log_role
1258
-
1301
+
1259
1302
  self._log_role = iam.Role(
1260
1303
  self.scope,
1261
1304
  "ApiGatewayLogRole",
@@ -1266,44 +1309,48 @@ class ApiGatewayIntegrationUtility:
1266
1309
  )
1267
1310
  ],
1268
1311
  )
1269
-
1312
+
1270
1313
  return self._log_role
1271
-
1314
+
1272
1315
  def _get_log_format(self) -> apigateway.AccessLogFormat:
1273
1316
  """Get access log format for API Gateway"""
1274
1317
  return apigateway.AccessLogFormat.custom(
1275
- json.dumps({
1276
- "requestId": "$context.requestId",
1277
- "extendedRequestId": "$context.extendedRequestId",
1278
- "method": "$context.httpMethod",
1279
- "route": "$context.resourcePath",
1280
- "status": "$context.status",
1281
- "requestBody": "$input.body",
1282
- "responseBody": "$context.responseLength",
1283
- "headers": "$context.requestHeaders",
1284
- "requestContext": "$context.requestContext",
1285
- })
1318
+ json.dumps(
1319
+ {
1320
+ "requestId": "$context.requestId",
1321
+ "extendedRequestId": "$context.extendedRequestId",
1322
+ "method": "$context.httpMethod",
1323
+ "route": "$context.resourcePath",
1324
+ "status": "$context.status",
1325
+ "requestBody": "$input.body",
1326
+ "responseBody": "$context.responseLength",
1327
+ "headers": "$context.requestHeaders",
1328
+ "requestContext": "$context.requestContext",
1329
+ }
1330
+ )
1286
1331
  )
1287
-
1288
- def group_integrations_by_api_gateway(self, integrations: List[Dict[str, Any]]) -> Dict[int, Dict[str, Any]]:
1332
+
1333
+ def group_integrations_by_api_gateway(
1334
+ self, integrations: List[Dict[str, Any]]
1335
+ ) -> Dict[int, Dict[str, Any]]:
1289
1336
  """Group integrations by API Gateway using object identity"""
1290
1337
  api_gateways = {}
1291
1338
  api_counter = 0
1292
-
1339
+
1293
1340
  for integration in integrations:
1294
- api_gateway = integration.get('api_gateway')
1341
+ api_gateway = integration.get("api_gateway")
1295
1342
  if api_gateway:
1296
1343
  # Use object identity as key instead of CDK token
1297
1344
  api_key = id(api_gateway)
1298
1345
  if api_key not in api_gateways:
1299
1346
  api_counter += 1
1300
1347
  api_gateways[api_key] = {
1301
- 'api_gateway': api_gateway,
1302
- 'integrations': [],
1303
- 'counter': api_counter
1348
+ "api_gateway": api_gateway,
1349
+ "integrations": [],
1350
+ "counter": api_counter,
1304
1351
  }
1305
- api_gateways[api_key]['integrations'].append(integration)
1306
-
1352
+ api_gateways[api_key]["integrations"].append(integration)
1353
+
1307
1354
  return api_gateways
1308
1355
 
1309
1356
  def _validate_and_adjust_authorization_configuration(
@@ -1311,39 +1358,41 @@ class ApiGatewayIntegrationUtility:
1311
1358
  ) -> ApiGatewayConfigRouteConfig:
1312
1359
  """
1313
1360
  Validate and adjust authorization configuration for security and clarity.
1314
-
1361
+
1315
1362
  This method implements 'secure by default' with explicit overrides:
1316
1363
  - If Cognito is available and route wants NONE auth, requires explicit override
1317
1364
  - If Cognito is not available and route wants COGNITO auth, raises error
1318
1365
  - Provides verbose warnings for monitoring and security awareness
1319
1366
  - Returns a potentially modified api_config with adjusted authorization_type
1320
-
1367
+
1321
1368
  Args:
1322
1369
  api_config (ApiGatewayConfigRouteConfig): Route configuration
1323
1370
  has_cognito_authorizer (bool): Whether a Cognito authorizer is configured
1324
-
1371
+
1325
1372
  Returns:
1326
1373
  ApiGatewayConfigRouteConfig: Potentially modified configuration
1327
-
1374
+
1328
1375
  Raises:
1329
1376
  ValueError: When there are security conflicts without explicit overrides
1330
1377
  """
1331
1378
  import logging
1332
1379
  from copy import deepcopy
1333
-
1380
+
1334
1381
  # Create a copy to avoid modifying the original
1335
1382
  modified_config = deepcopy(api_config)
1336
-
1337
- auth_type = getattr(api_config, 'authorization_type', 'COGNITO')
1338
-
1383
+
1384
+ auth_type = str(getattr(api_config, "authorization_type", "COGNITO")).upper()
1385
+
1339
1386
  # Check for explicit override flag
1340
- explicit_override = getattr(api_config, 'allow_public_override', False)
1341
-
1342
- route_path = getattr(api_config, 'routes', 'unknown')
1343
- method = getattr(api_config, 'method', 'unknown')
1344
-
1387
+ explicit_override = (
1388
+ str(getattr(api_config, "allow_public_override", False)).lower() == "true"
1389
+ )
1390
+
1391
+ route_path = getattr(api_config, "routes", "unknown")
1392
+ method = getattr(api_config, "method", "unknown")
1393
+
1345
1394
  logger = logging.getLogger(__name__)
1346
-
1395
+
1347
1396
  # Case 1: Cognito available + NONE requested + No explicit override = ERROR
1348
1397
  if has_cognito_authorizer and auth_type == "NONE" and not explicit_override:
1349
1398
  error_msg = (
@@ -1355,16 +1404,17 @@ class ApiGatewayIntegrationUtility:
1355
1404
  f" 1. Remove Cognito configuration if you want public access\n"
1356
1405
  f" 2. Add 'allow_public_override': true to explicitly allow public access\n"
1357
1406
  f" 3. Remove 'authorization_type': 'NONE' to use secure Cognito auth\n\n"
1358
- f"🔒 This prevents accidental public endpoints when authentication is available."
1407
+ f"🔒 This prevents accidental public endpoints when authentication is available.\n\n"
1408
+ f"👉 ApiGatewayIntegrationUtility documentation for more details: https://github.com/your-repo/api-gateway-stack"
1359
1409
  )
1360
1410
  raise ValueError(error_msg)
1361
-
1362
- # Case 2: No Cognito + COGNITO explicitly requested = ERROR
1411
+
1412
+ # Case 2: No Cognito + COGNITO explicitly requested = ERROR
1363
1413
  # Only error if COGNITO was explicitly requested, not if it's the default
1364
1414
  original_auth_type = None
1365
- if hasattr(api_config, 'dictionary') and api_config.dictionary:
1366
- original_auth_type = api_config.dictionary.get('authorization_type')
1367
-
1415
+ if hasattr(api_config, "dictionary") and api_config.dictionary:
1416
+ original_auth_type = api_config.dictionary.get("authorization_type")
1417
+
1368
1418
  if not has_cognito_authorizer and original_auth_type == "COGNITO":
1369
1419
  error_msg = (
1370
1420
  f"🚨 CONFIGURATION ERROR for route {route_path} ({method}):\n"
@@ -1377,7 +1427,7 @@ class ApiGatewayIntegrationUtility:
1377
1427
  f" 4. Remove explicit authorization_type to use default behavior"
1378
1428
  )
1379
1429
  raise ValueError(error_msg)
1380
-
1430
+
1381
1431
  # Case 3: Cognito available + NONE requested + Explicit override = WARN
1382
1432
  if has_cognito_authorizer and auth_type == "NONE" and explicit_override:
1383
1433
  warning_msg = (
@@ -1387,10 +1437,10 @@ class ApiGatewayIntegrationUtility:
1387
1437
  f" 📊 Consider monitoring this endpoint for unexpected usage patterns\n"
1388
1438
  f" 🔍 Review periodically: Should this endpoint be secured?"
1389
1439
  )
1390
-
1440
+
1391
1441
  # Print to console during deployment for visibility
1392
1442
  print(warning_msg)
1393
-
1443
+
1394
1444
  # Structured logging for monitoring and metrics
1395
1445
  logger.warning(
1396
1446
  "Public endpoint configured with Cognito available",
@@ -1402,18 +1452,22 @@ class ApiGatewayIntegrationUtility:
1402
1452
  "authorization_type": "NONE",
1403
1453
  "metric_name": "public_endpoint_with_cognito",
1404
1454
  "security_decision": "intentional_public",
1405
- "recommendation": "review_periodically"
1406
- }
1455
+ "recommendation": "review_periodically",
1456
+ },
1407
1457
  )
1408
-
1458
+
1409
1459
  # Case 4: No Cognito + default COGNITO = Fall back to NONE
1410
- if not has_cognito_authorizer and auth_type == "COGNITO" and original_auth_type is None:
1460
+ if (
1461
+ not has_cognito_authorizer
1462
+ and auth_type == "COGNITO"
1463
+ and original_auth_type is None
1464
+ ):
1411
1465
  modified_config.authorization_type = "NONE"
1412
1466
  logger.info(
1413
1467
  f"No Cognito authorizer available for route {route_path} ({method}), "
1414
1468
  f"defaulting to public access (NONE authorization)"
1415
1469
  )
1416
-
1470
+
1417
1471
  # Case 5: No Cognito + NONE = INFO (expected for public-only APIs)
1418
1472
  if not has_cognito_authorizer and auth_type == "NONE":
1419
1473
  logger.info(
@@ -1423,8 +1477,8 @@ class ApiGatewayIntegrationUtility:
1423
1477
  "method": method,
1424
1478
  "authorization_type": "NONE",
1425
1479
  "cognito_available": False,
1426
- "security_decision": "public_only_api"
1427
- }
1480
+ "security_decision": "public_only_api",
1481
+ },
1428
1482
  )
1429
-
1483
+
1430
1484
  return modified_config
cdk_factory/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.7.26"
1
+ __version__ = "0.7.27"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cdk_factory
3
- Version: 0.7.26
3
+ Version: 0.7.27
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
@@ -1,7 +1,7 @@
1
1
  cdk_factory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  cdk_factory/app.py,sha256=xv863N7O6HPKznB68_t7O4la9JacrkG87t9TjoDUk7s,2827
3
3
  cdk_factory/cdk.json,sha256=SKZKhJ2PBpFH78j-F8S3VDYW-lf76--Q2I3ON-ZIQfw,3106
4
- cdk_factory/version.py,sha256=bdHnwFVnv19lO1d9o0Rjk5Js7MqJglg3CX1USYBiklY,23
4
+ cdk_factory/version.py,sha256=fHi3FiqQRMkjl3o8ATyV9gZrBSNTVvt5m0WxJ-NYHTA,23
5
5
  cdk_factory/builds/README.md,sha256=9BBWd7bXpyKdMU_g2UljhQwrC9i5O_Tvkb6oPvndoZk,90
6
6
  cdk_factory/commands/command_loader.py,sha256=QbLquuP_AdxtlxlDy-2IWCQ6D-7qa58aphnDPtp_uTs,3744
7
7
  cdk_factory/configurations/base_config.py,sha256=JKjhNsy0RCUZy1s8n5D_aXXI-upR9izaLtCTfKYiV9k,9624
@@ -18,7 +18,7 @@ cdk_factory/configurations/stack.py,sha256=7whhC48dUYw7BBFV49zM1Q3AghTNkaiDfy4kK
18
18
  cdk_factory/configurations/workload.py,sha256=sM-B6UKOdOn5_H-eWmW03J9oa8YZZmO0bvQ69wbCM0Q,7756
19
19
  cdk_factory/configurations/resources/_resources.py,sha256=tnXGn4kEC0JPQaTWB3QpAZG-2hIGBtugHTzuKn1OTvE,2548
20
20
  cdk_factory/configurations/resources/api_gateway.py,sha256=-k4hMGszIdQLb5DGmWBIPy49YGutp8zczafRh-Vob0I,4904
21
- cdk_factory/configurations/resources/apigateway_route_config.py,sha256=fESVFKEKBj-EdkxB-iyDcA43FY1fX9Ow1QedH6UCm2I,2328
21
+ cdk_factory/configurations/resources/apigateway_route_config.py,sha256=6ytn_nwKwlfpBtHL5sV6gxMpgAJ3p6QFGumMoW4CTHM,2351
22
22
  cdk_factory/configurations/resources/auto_scaling.py,sha256=OAVl8iUdHiOYVzme1qNDwA3w2raxDNUo_W2_Vebqtx8,5005
23
23
  cdk_factory/configurations/resources/cloudfront.py,sha256=xwDIrYQDqQMgekXSJ5vrgNXIUCfY6O8aiybE5ewwijw,1055
24
24
  cdk_factory/configurations/resources/cloudwatch_widget.py,sha256=EdEQSXUkDtoY_Mg_cJBWo1Hp84jSiK7U9tsd3k1VhKI,1271
@@ -72,7 +72,7 @@ cdk_factory/stack/stack_module_registry.py,sha256=J14-A75VZESzRQa8p-Fepdap7Z8T7m
72
72
  cdk_factory/stack/stack_modules.py,sha256=kgEK-j0smZPozVwTCfM1g1V17EyTBT0TXAQZq4vZz0o,784
73
73
  cdk_factory/stack_library/__init__.py,sha256=5Y9TpIe8ZK1688G60PGcuP-hM0RvYEY_3Hl2qJCJJrw,581
74
74
  cdk_factory/stack_library/stack_base.py,sha256=tTleSFmlf26DuKVF_ytftf8P7IVWb5iex8cYfYupfvQ,4940
75
- cdk_factory/stack_library/api_gateway/api_gateway_stack.py,sha256=i87MGWAqRJBrayQaXVf-R90_NKVZpbOf3QNibnOIsBM,35507
75
+ cdk_factory/stack_library/api_gateway/api_gateway_stack.py,sha256=-K9ybNHpjycw4wtaiWbMm9dLCYPYoaqlakgBIQyotCk,35632
76
76
  cdk_factory/stack_library/auto_scaling/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
77
  cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py,sha256=UsFqUb_3XPJAlmZ6F75nXna3elOggD1KuFmmdmhi0Lg,19070
78
78
  cdk_factory/stack_library/aws_lambdas/lambda_stack.py,sha256=xT8-799fSXMpYaRhRKytxsbpb5EG3g2h6L822_RpRSk,22956
@@ -97,7 +97,7 @@ cdk_factory/stack_library/vpc/__init__.py,sha256=7pIqP97Gf2AJbv9Ebp1WbQGHYhgEbWJ
97
97
  cdk_factory/stack_library/vpc/vpc_stack.py,sha256=zdDiGilf03esxuya5Z8zVYSVMAIuZBeD-ZKgfnEd6aw,10077
98
98
  cdk_factory/stack_library/websites/static_website_stack.py,sha256=KBQiV6PI09mpHGtH-So5Hk3uhfFLDepoXInGbfin0cY,7938
99
99
  cdk_factory/stages/websites/static_website_stage.py,sha256=X4fpKXkhb0zIbSHx3QyddBhVSLBryb1vf1Cg2fMTqog,755
100
- cdk_factory/utilities/api_gateway_integration_utility.py,sha256=QlIGmWAJShDMTy7mIJeQDyHr59jP7EKQQOFXTNCQJA4,61923
100
+ cdk_factory/utilities/api_gateway_integration_utility.py,sha256=LSfw2lNtJWMsRiaHf4FLiGHRgFPm-fOUhN2YJNHtxnI,62346
101
101
  cdk_factory/utilities/commandline_args.py,sha256=0FiNEJFbWVN8Ct7r0VHnJEx7rhUlaRKT7R7HMNJBSTI,2216
102
102
  cdk_factory/utilities/configuration_loader.py,sha256=z0ZdGLNbTO4_yfluB9zUh_i_Poc9qj-7oRyjMRlNkN8,1522
103
103
  cdk_factory/utilities/docker_utilities.py,sha256=9r8C-lXYpymqEfi3gTeWCQzHldvfjttPqn6p3j2khTE,8111
@@ -109,7 +109,7 @@ cdk_factory/utilities/lambda_function_utilities.py,sha256=j3tBdv_gC2MdEwBINDwAqY
109
109
  cdk_factory/utilities/os_execute.py,sha256=5Op0LY_8Y-pUm04y1k8MTpNrmQvcLmQHPQITEP7EuSU,1019
110
110
  cdk_factory/utils/api_gateway_utilities.py,sha256=If7Xu5s_UxmuV-kL3JkXxPLBdSVUKoLtohm0IUFoiV8,4378
111
111
  cdk_factory/workload/workload_factory.py,sha256=yBUDGIuB8-5p_mGcVFxsD2ZoZIziak3yh3LL3JvS0M4,5903
112
- cdk_factory-0.7.26.dist-info/METADATA,sha256=33JcY3G617lMyKEaMalJobLMN5SQdNSqCSdHp5pgeDc,2451
113
- cdk_factory-0.7.26.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
114
- cdk_factory-0.7.26.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
115
- cdk_factory-0.7.26.dist-info/RECORD,,
112
+ cdk_factory-0.7.27.dist-info/METADATA,sha256=kkqu-N4uFgjOYSenuas-fjiwtIcTNoxmRK8NR6CBsyw,2451
113
+ cdk_factory-0.7.27.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
114
+ cdk_factory-0.7.27.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
115
+ cdk_factory-0.7.27.dist-info/RECORD,,