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

@@ -60,7 +60,7 @@ class LiveSsmResolver:
60
60
  Resolve SSM parameter with live API call.
61
61
 
62
62
  Args:
63
- parameter_path: SSM parameter path (e.g., /movatra/dev/cognito/user-pool/user-pool-arn)
63
+ parameter_path: SSM parameter path (e.g., /workload/dev/cognito/user-pool/user-pool-arn)
64
64
  fallback_value: Value to return if live resolution fails
65
65
 
66
66
  Returns:
@@ -347,15 +347,125 @@ class ApiGatewayStack(IStack, EnhancedSsmParameterMixin):
347
347
  # Setup CORS using centralized utility
348
348
  self.integration_utility.setup_route_cors(resource, route_path, route)
349
349
 
350
+ def _validate_authorization_configuration(self, route, has_cognito_authorizer):
351
+ """
352
+ Validate authorization configuration for security and clarity.
353
+
354
+ This method implements 'secure by default' with explicit overrides:
355
+ - If Cognito is available and route wants NONE auth, requires explicit override
356
+ - If Cognito is not available and route wants COGNITO auth, raises error
357
+ - Provides verbose warnings for monitoring and security awareness
358
+
359
+ Args:
360
+ route (dict): Route configuration
361
+ has_cognito_authorizer (bool): Whether a Cognito authorizer is configured
362
+
363
+ Raises:
364
+ ValueError: When there are security conflicts without explicit overrides
365
+ """
366
+ import logging
367
+
368
+ auth_type = route.get("authorization_type", "COGNITO")
369
+ explicit_override = route.get("allow_public_override", False)
370
+ route_path = route.get("path", "unknown")
371
+ method = route.get("method", "unknown")
372
+
373
+ logger = logging.getLogger(__name__)
374
+
375
+ # Case 1: Cognito available + NONE requested + No explicit override = ERROR
376
+ if has_cognito_authorizer and auth_type == "NONE" and not explicit_override:
377
+ error_msg = (
378
+ f"🚨 SECURITY CONFLICT DETECTED for route {route_path} ({method}):\n"
379
+ f" ❌ Cognito authorizer is configured (manual or auto-import)\n"
380
+ f" ❌ authorization_type is set to 'NONE' (public access)\n"
381
+ f" ❌ This creates a security risk - public endpoint with auth available\n\n"
382
+ f"💡 SOLUTIONS:\n"
383
+ f" 1. Remove Cognito configuration if you want public access\n"
384
+ f" 2. Add 'allow_public_override': true to explicitly allow public access\n"
385
+ f" 3. Remove 'authorization_type': 'NONE' to use secure Cognito auth\n\n"
386
+ f"🔒 This prevents accidental public endpoints when authentication is available."
387
+ )
388
+ raise ValueError(error_msg)
389
+
390
+ # Case 2: No Cognito + COGNITO explicitly requested = ERROR
391
+ # Only error if COGNITO was explicitly requested, not if it's the default
392
+ if not has_cognito_authorizer and route.get("authorization_type") == "COGNITO":
393
+ error_msg = (
394
+ f"🚨 CONFIGURATION ERROR for route {route_path} ({method}):\n"
395
+ f" ❌ authorization_type is explicitly set to 'COGNITO' but no Cognito authorizer configured\n"
396
+ f" ❌ Cannot secure endpoint without authentication provider\n\n"
397
+ f"💡 SOLUTIONS:\n"
398
+ f" 1. Add Cognito configuration to enable authentication\n"
399
+ f" 2. Set authorization_type to 'NONE' for public access\n"
400
+ f" 3. Configure SSM auto-import for user_pool_arn\n"
401
+ f" 4. Remove explicit authorization_type to use default behavior"
402
+ )
403
+ raise ValueError(error_msg)
404
+
405
+ # Case 3: Cognito available + NONE requested + Explicit override = WARN
406
+ if has_cognito_authorizer and auth_type == "NONE" and explicit_override:
407
+ warning_msg = (
408
+ f"⚠️ PUBLIC ENDPOINT CONFIGURED: {route_path} ({method})\n"
409
+ f" 🔓 This endpoint is intentionally public (allow_public_override: true)\n"
410
+ f" 🔐 Cognito authentication is available but overridden\n"
411
+ f" 📊 Consider monitoring this endpoint for unexpected usage patterns\n"
412
+ f" 🔍 Review periodically: Should this endpoint be secured?"
413
+ )
414
+
415
+ # Print to console during deployment for visibility
416
+ print(warning_msg)
417
+
418
+ # Structured logging for monitoring and metrics
419
+ logger.warning(
420
+ "Public endpoint configured with Cognito available",
421
+ extra={
422
+ "route": route_path,
423
+ "method": method,
424
+ "security_override": True,
425
+ "cognito_available": True,
426
+ "authorization_type": "NONE",
427
+ "metric_name": "public_endpoint_with_cognito",
428
+ "security_decision": "intentional_public",
429
+ "recommendation": "review_periodically"
430
+ }
431
+ )
432
+
433
+ # Case 4: No Cognito + NONE = INFO (expected for public-only APIs)
434
+ if not has_cognito_authorizer and auth_type == "NONE":
435
+ logger.info(
436
+ f"Public endpoint configured (no Cognito available): {route_path} ({method})",
437
+ extra={
438
+ "route": route_path,
439
+ "method": method,
440
+ "authorization_type": "NONE",
441
+ "cognito_available": False,
442
+ "security_decision": "public_only_api"
443
+ }
444
+ )
445
+
350
446
  def _setup_lambda_integration(
351
447
  self, api_gateway, api_id, route, lambda_fn, authorizer, suffix
352
448
  ):
353
449
  """Setup Lambda integration for a route"""
450
+ import logging
451
+
354
452
  route_path = route["path"]
355
453
  # Secure by default: require Cognito authorization unless explicitly set to NONE
356
454
  authorization_type = route.get("authorization_type", "COGNITO")
357
455
 
358
- # If explicitly set to NONE, skip authorization
456
+ # 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:
458
+ authorization_type = "NONE"
459
+ logger = logging.getLogger(__name__)
460
+ logger.info(
461
+ f"No Cognito authorizer available for route {route_path} ({route.get('method', 'unknown')}), "
462
+ f"defaulting to public access (NONE authorization)"
463
+ )
464
+
465
+ # Validate authorization configuration for security
466
+ self._validate_authorization_configuration(route, authorizer is not None)
467
+
468
+ # If set to NONE (explicitly or by fallback), skip authorization
359
469
  if authorization_type == "NONE":
360
470
  authorizer = None
361
471
 
@@ -391,9 +501,23 @@ class ApiGatewayStack(IStack, EnhancedSsmParameterMixin):
391
501
  self, api_gateway, route, lambda_fn, authorizer, api_id, suffix
392
502
  ):
393
503
  """Setup fallback Lambda integration for routes without src"""
504
+ import logging
505
+
394
506
  route_path = route["path"]
395
507
  # Secure by default: require Cognito authorization unless explicitly set to NONE
396
508
  authorization_type = route.get("authorization_type", "COGNITO")
509
+
510
+ # 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:
512
+ authorization_type = "NONE"
513
+ logger = logging.getLogger(__name__)
514
+ logger.info(
515
+ f"No Cognito authorizer available for route {route_path} ({route.get('method', 'unknown')}), "
516
+ f"defaulting to public access (NONE authorization)"
517
+ )
518
+
519
+ # Validate authorization configuration for security
520
+ self._validate_authorization_configuration(route, authorizer is not None)
397
521
 
398
522
  resource = (
399
523
  api_gateway.root.resource_for_path(route_path)
@@ -68,7 +68,7 @@ class ApiGatewayIntegrationUtility:
68
68
  )
69
69
 
70
70
  # Add method to API Gateway
71
- resource = self.get_or_create_resource(api_gateway, api_config.routes)
71
+ resource = self.get_or_create_resource(api_gateway, api_config.routes, stack_config)
72
72
 
73
73
  # Handle existing authorizer ID using L1 constructs
74
74
  if self._get_existing_authorizer_id_with_ssm_fallback(api_config, stack_config):
@@ -602,12 +602,20 @@ class ApiGatewayIntegrationUtility:
602
602
  return self.authorizer
603
603
 
604
604
  def get_or_create_resource(
605
- self, api_gateway: apigateway.RestApi, route_path: str
605
+ self, api_gateway: apigateway.RestApi, route_path: str, stack_config=None
606
606
  ) -> apigateway.Resource:
607
- """Get or create API Gateway resource for the given route path"""
607
+ """Get or create API Gateway resource for the given route path with cross-stack support"""
608
608
  if not route_path or route_path == "/":
609
609
  return api_gateway.root
610
610
 
611
+ # Check for existing resource import configuration
612
+ if stack_config:
613
+ api_gateway_config = stack_config.dictionary.get("api_gateway", {})
614
+ existing_resources = api_gateway_config.get("existing_resources", {})
615
+
616
+ if existing_resources:
617
+ return self._create_resource_with_imports(api_gateway, route_path, existing_resources)
618
+
611
619
  # Use the built-in resource_for_path method which handles existing resources correctly
612
620
  try:
613
621
  # This method automatically creates the full path and reuses existing resources
@@ -658,6 +666,94 @@ class ApiGatewayIntegrationUtility:
658
666
 
659
667
  return current_resource
660
668
 
669
+ def _create_resource_with_imports(
670
+ self, api_gateway: apigateway.RestApi, route_path: str, existing_resources: dict
671
+ ) -> apigateway.Resource:
672
+ """Create resource path using existing resource imports to avoid conflicts"""
673
+ from aws_cdk import aws_apigateway as apigateway
674
+
675
+ # Remove leading slash and split path
676
+ path_parts = route_path.lstrip("/").split("/")
677
+ current_resource = api_gateway.root
678
+ current_path = ""
679
+
680
+ # Navigate through path parts, importing existing resources where configured
681
+ for i, part in enumerate(path_parts):
682
+ if not part: # Skip empty parts
683
+ continue
684
+
685
+ current_path = "/" + "/".join(path_parts[:i+1])
686
+
687
+ # Check if this path segment should be imported from existing resources
688
+ if current_path in existing_resources:
689
+ resource_config = existing_resources[current_path]
690
+ resource_id = resource_config.get("resource_id")
691
+
692
+ if resource_id:
693
+ logger.info(f"Importing existing resource for path {current_path} with ID: {resource_id}")
694
+
695
+ # Import the existing resource using L1 constructs
696
+ current_resource = self._import_existing_resource(
697
+ api_gateway, current_resource, part, resource_id, current_path
698
+ )
699
+ else:
700
+ # Create normally if no resource_id specified
701
+ current_resource = self._add_resource_safely(current_resource, part)
702
+ else:
703
+ # Create normally for non-imported paths
704
+ current_resource = self._add_resource_safely(current_resource, part)
705
+
706
+ return current_resource
707
+
708
+ def _import_existing_resource(
709
+ self, api_gateway: apigateway.RestApi, parent_resource: apigateway.Resource,
710
+ path_part: str, resource_id: str, full_path: str
711
+ ) -> apigateway.Resource:
712
+ """Import an existing API Gateway resource by ID"""
713
+ from aws_cdk import aws_apigateway as apigateway
714
+
715
+ try:
716
+ # Use CfnResource to reference existing resource
717
+ # This creates a reference without trying to create the resource
718
+ imported_resource = apigateway.Resource.from_resource_id(
719
+ self.scope,
720
+ f"imported-resource-{hash(full_path) % 10000}",
721
+ resource_id
722
+ )
723
+
724
+ logger.info(f"Successfully imported existing resource: {path_part} (ID: {resource_id})")
725
+ return imported_resource
726
+
727
+ except Exception as e:
728
+ logger.warning(f"Failed to import resource {path_part} with ID {resource_id}: {e}")
729
+ # Fallback to normal creation
730
+ return self._add_resource_safely(parent_resource, path_part)
731
+
732
+ def _add_resource_safely(
733
+ self, parent_resource: apigateway.Resource, path_part: str
734
+ ) -> apigateway.Resource:
735
+ """Add resource with conflict handling"""
736
+ try:
737
+ return parent_resource.add_resource(path_part)
738
+ except Exception as e:
739
+ if "AlreadyExists" in str(e) or "same parent already has this name" in str(e):
740
+ logger.warning(f"Resource {path_part} already exists, attempting to find existing resource")
741
+
742
+ # Try to find the existing resource in children
743
+ for child in parent_resource.node.children:
744
+ if (
745
+ hasattr(child, "path_part")
746
+ and getattr(child, "path_part", None) == path_part
747
+ ):
748
+ logger.info(f"Found existing resource: {path_part}")
749
+ return child
750
+
751
+ # If not found in children, re-raise the error
752
+ logger.error(f"Could not find or create resource: {path_part}")
753
+ raise e
754
+ else:
755
+ raise e
756
+
661
757
  def _get_existing_api_gateway_id_with_ssm_fallback(
662
758
  self, api_config: ApiGatewayConfigRouteConfig, stack_config
663
759
  ) -> Optional[str]:
cdk_factory/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.7.22"
1
+ __version__ = "0.7.24"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cdk_factory
3
- Version: 0.7.22
3
+ Version: 0.7.24
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=05OpHzT8pDPhLRXPkdqyUUquJk9jolNT5B2C6ovgfac,23
4
+ cdk_factory/version.py,sha256=6LhdCdekRyhf1ou0xLzDta4kGS27Isdvy5haWR1XJoc,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
@@ -58,7 +58,7 @@ cdk_factory/constructs/s3_buckets/s3_bucket_replication_source_construct.py,sha2
58
58
  cdk_factory/constructs/sqs/policies/sqs_policies.py,sha256=4p0G8G-fqNKSr68I55fvqH-DkhIeXyGaFBKkICIJ-qM,1277
59
59
  cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py,sha256=k-jfwRJhIs6sxifmh7rFaX92tpHMdxiZ8mgPP4NNu5E,13041
60
60
  cdk_factory/interfaces/istack.py,sha256=bhTBs-o9FgKwvJMSuwxjUV6D3nUlvZHVzfm27jP9x9Y,987
61
- cdk_factory/interfaces/live_ssm_resolver.py,sha256=UjKeubz-05zlHcqsaYA6Vx-XuTCKRWVkURXRkVvgqzo,8162
61
+ cdk_factory/interfaces/live_ssm_resolver.py,sha256=3FIr9a02SXqZmbFs3RT0WxczWEQR_CF7QSt7kWbDrVE,8163
62
62
  cdk_factory/interfaces/ssm_parameter_mixin.py,sha256=uA2j8HmAOpuEA9ynRj51s0WjUHMVLsbLQN-QS9NKyHA,12089
63
63
  cdk_factory/lambdas/health_handler.py,sha256=dd40ykKMxWCFEIyp2ZdQvAGNjw_ylI9CSm1N24Hp2ME,196
64
64
  cdk_factory/pipeline/pipeline_factory.py,sha256=DtvGlCjq1uNafwtNevbpwgIR4Du8UoaTVqfgqC_FePU,15430
@@ -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=LEjDNcCXpD2rqdi66ef1rbg6uTdt18WnZZcr__Wxjzw,29106
75
+ cdk_factory/stack_library/api_gateway/api_gateway_stack.py,sha256=i87MGWAqRJBrayQaXVf-R90_NKVZpbOf3QNibnOIsBM,35507
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=g6BPg3gSRTijPlUzYvWt0ctnYazDtzUtfqGHvdvpQHQ,50668
100
+ cdk_factory/utilities/api_gateway_integration_utility.py,sha256=RdStGFueFFDR_j1zHX-d55czZKf_lP-_Ty_5-XLPQXg,55224
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.22.dist-info/METADATA,sha256=B5DMrfGsL7hszQJO41UIS0fzbhfwuUqIMgAizfCiEIs,2451
113
- cdk_factory-0.7.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
114
- cdk_factory-0.7.22.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
115
- cdk_factory-0.7.22.dist-info/RECORD,,
112
+ cdk_factory-0.7.24.dist-info/METADATA,sha256=arpwSUyP9MEQqdJM6dkFtxxq0Q3o3PvjutwymPmzV8g,2451
113
+ cdk_factory-0.7.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
114
+ cdk_factory-0.7.24.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
115
+ cdk_factory-0.7.24.dist-info/RECORD,,