|
1 | 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
2 | 2 | # SPDX-License-Identifier: MIT-0 |
3 | 3 |
|
4 | | -from typing import Optional |
| 4 | +import json |
5 | 5 | import os |
6 | 6 | import subprocess |
7 | | -import datetime |
8 | 7 | import time |
9 | | -import json |
| 8 | +from typing import Optional |
| 9 | + |
10 | 10 | import boto3 |
11 | 11 | from botocore.exceptions import ClientError |
12 | 12 | from loguru import logger |
13 | 13 |
|
| 14 | + |
14 | 15 | class InstallService(): |
15 | 16 | def __init__(self, |
16 | 17 | account_id: str, |
@@ -195,7 +196,7 @@ def cleanup_failed_stack(self, stack_name): |
195 | 196 | logger.error(f"Failed to cleanup stack {stack_name}: {e}") |
196 | 197 | return False |
197 | 198 |
|
198 | | - def get_existing_service_role_arn(self): |
| 199 | + def get_service_role_arn(self): |
199 | 200 | """ |
200 | 201 | Check if CloudFormation service role stack exists and return its ARN. |
201 | 202 | |
@@ -237,17 +238,11 @@ def get_existing_service_role_arn(self): |
237 | 238 |
|
238 | 239 | def deploy_service_role(self): |
239 | 240 | """ |
240 | | - Deploy the CloudFormation service role stack only if it doesn't exist. |
| 241 | + Deploy the CloudFormation service role stack. |
241 | 242 | |
242 | 243 | Returns: |
243 | 244 | str: The ARN of the service role, or None if deployment failed |
244 | 245 | """ |
245 | | - # First check if service role already exists |
246 | | - existing_arn = self.get_existing_service_role_arn() |
247 | | - if existing_arn: |
248 | | - logger.info("CloudFormation service role already exists, skipping deployment") |
249 | | - return existing_arn |
250 | | - |
251 | 246 | service_role_stack_name = f"{self.cfn_prefix}-cloudformation-service-role" |
252 | 247 | service_role_template = 'iam-roles/cloudformation-management/IDP-Cloudformation-Service-Role.yaml' |
253 | 248 |
|
@@ -284,7 +279,7 @@ def deploy_service_role(self): |
284 | 279 | logger.debug(f"Service role deploy stderr: {process.stderr}") |
285 | 280 |
|
286 | 281 | # Get the service role ARN from stack outputs |
287 | | - service_role_arn = self.get_existing_service_role_arn() |
| 282 | + service_role_arn = self.get_service_role_arn() |
288 | 283 | if service_role_arn: |
289 | 284 | logger.info(f"Successfully deployed service role: {service_role_arn}") |
290 | 285 | return service_role_arn |
@@ -359,27 +354,50 @@ def create_permission_boundary_policy(self): |
359 | 354 | return None |
360 | 355 |
|
361 | 356 | def validate_permission_boundary(self, stack_name, boundary_arn): |
362 | | - """Validate that all IAM roles in the stack have the permission boundary""" |
| 357 | + """Validate that all IAM roles in the stack and nested stacks have the permission boundary""" |
363 | 358 | cfn = boto3.client('cloudformation') |
364 | 359 | iam = boto3.client('iam') |
365 | 360 |
|
| 361 | + def get_all_stacks(stack_name): |
| 362 | + """Recursively get all nested stacks""" |
| 363 | + stacks = [stack_name] |
| 364 | + try: |
| 365 | + paginator = cfn.get_paginator('list_stack_resources') |
| 366 | + page_iterator = paginator.paginate(StackName=stack_name) |
| 367 | + |
| 368 | + for page in page_iterator: |
| 369 | + for resource in page['StackResourceSummaries']: |
| 370 | + if resource['ResourceType'] == 'AWS::CloudFormation::Stack': |
| 371 | + nested_stack_name = resource['PhysicalResourceId'] |
| 372 | + stacks.extend(get_all_stacks(nested_stack_name)) |
| 373 | + except ClientError: |
| 374 | + pass |
| 375 | + return stacks |
| 376 | + |
366 | 377 | try: |
367 | | - # Get all IAM roles in the stack |
368 | | - paginator = cfn.get_paginator('list_stack_resources') |
369 | | - page_iterator = paginator.paginate(StackName=stack_name) |
| 378 | + # Get all stacks (main + nested) |
| 379 | + all_stacks = get_all_stacks(stack_name) |
| 380 | + logger.info(f"Checking {len(all_stacks)} stacks for IAM roles") |
370 | 381 |
|
371 | 382 | roles = [] |
372 | | - for page in page_iterator: |
373 | | - for resource in page['StackResourceSummaries']: |
374 | | - if resource['ResourceType'] == 'AWS::IAM::Role': |
375 | | - role_name = resource['PhysicalResourceId'] |
376 | | - roles.append(role_name) |
| 383 | + for stack in all_stacks: |
| 384 | + try: |
| 385 | + paginator = cfn.get_paginator('list_stack_resources') |
| 386 | + page_iterator = paginator.paginate(StackName=stack) |
| 387 | + |
| 388 | + for page in page_iterator: |
| 389 | + for resource in page['StackResourceSummaries']: |
| 390 | + if resource['ResourceType'] == 'AWS::IAM::Role': |
| 391 | + role_name = resource['PhysicalResourceId'] |
| 392 | + roles.append(role_name) |
| 393 | + except ClientError: |
| 394 | + continue |
377 | 395 |
|
378 | 396 | if not roles: |
379 | | - logger.info("No IAM roles found in the stack") |
| 397 | + logger.info("No IAM roles found in any stack") |
380 | 398 | return True |
381 | 399 |
|
382 | | - logger.info(f"Found {len(roles)} IAM roles in the stack") |
| 400 | + logger.info(f"Found {len(roles)} IAM roles across all stacks") |
383 | 401 | failed_roles = [] |
384 | 402 |
|
385 | 403 | # Check each role |
@@ -433,11 +451,11 @@ def install(self, admin_email: str, idp_pattern: str): |
433 | 451 | logger.error("Failed to create permission boundary policy. Aborting deployment.") |
434 | 452 | return False |
435 | 453 |
|
436 | | - # Step 2: Ensure CloudFormation service role exists |
437 | | - logger.info("Step 2: Ensuring CloudFormation service role exists...") |
| 454 | + # Step 2: Deploy CloudFormation service role |
| 455 | + logger.info("Step 2: Deploying CloudFormation service role...") |
438 | 456 | service_role_arn = self.deploy_service_role() |
439 | 457 | if not service_role_arn: |
440 | | - logger.error("Failed to deploy or find service role. Aborting IDP deployment.") |
| 458 | + logger.error("Failed to deploy service role. Aborting IDP deployment.") |
441 | 459 | return False |
442 | 460 |
|
443 | 461 | # Step 3: Deploy IDP stack using the service role and permission boundary |
|
0 commit comments