66import subprocess
77import datetime
88import time
9+ import json
10+ import boto3
11+ from botocore .exceptions import ClientError
912from loguru import logger
1013
1114class InstallService ():
@@ -307,9 +310,113 @@ def deploy_service_role(self):
307310 logger .error (f"Unexpected error during service role deployment: { e } " )
308311 return None
309312
313+ def create_permission_boundary_policy (self ):
314+ """Create an 'allow everything' permission boundary policy if it doesn't exist"""
315+
316+ policy_name = "IDPPermissionBoundary"
317+ iam = boto3 .client ('iam' )
318+
319+ try :
320+ # First, check if the policy already exists
321+ account_id = boto3 .client ('sts' ).get_caller_identity ()['Account' ]
322+ policy_arn = f"arn:aws:iam::{ account_id } :policy/{ policy_name } "
323+
324+ # Try to get the existing policy
325+ iam .get_policy (PolicyArn = policy_arn )
326+ logger .info (f"Permission boundary policy already exists: { policy_arn } " )
327+ return policy_arn
328+
329+ except ClientError as e :
330+ if e .response ['Error' ]['Code' ] == 'NoSuchEntity' :
331+ # Policy doesn't exist, create it
332+ policy_document = {
333+ "Version" : "2012-10-17" ,
334+ "Statement" : [
335+ {
336+ "Effect" : "Allow" ,
337+ "Action" : "*" ,
338+ "Resource" : "*"
339+ }
340+ ]
341+ }
342+
343+ try :
344+ response = iam .create_policy (
345+ PolicyName = policy_name ,
346+ PolicyDocument = json .dumps (policy_document ),
347+ Description = "Permission boundary for IDP deployment - allows all actions"
348+ )
349+
350+ policy_arn = response ['Policy' ]['Arn' ]
351+ logger .info (f"Created permission boundary policy: { policy_arn } " )
352+ return policy_arn
353+
354+ except ClientError as create_error :
355+ logger .error (f"Error creating permission boundary policy: { create_error } " )
356+ return None
357+ else :
358+ logger .error (f"Error checking for existing permission boundary policy: { e } " )
359+ return None
360+
361+ def validate_permission_boundary (self , stack_name , boundary_arn ):
362+ """Validate that all IAM roles in the stack have the permission boundary"""
363+ cfn = boto3 .client ('cloudformation' )
364+ iam = boto3 .client ('iam' )
365+
366+ 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 )
370+
371+ 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 )
377+
378+ if not roles :
379+ logger .info ("No IAM roles found in the stack" )
380+ return True
381+
382+ logger .info (f"Found { len (roles )} IAM roles in the stack" )
383+ failed_roles = []
384+
385+ # Check each role
386+ for role_name in roles :
387+ try :
388+ response = iam .get_role (RoleName = role_name )
389+ role = response ['Role' ]
390+
391+ if 'PermissionsBoundary' in role :
392+ actual_boundary = role ['PermissionsBoundary' ]['PermissionsBoundaryArn' ]
393+ if actual_boundary == boundary_arn :
394+ logger .debug (f"✅ { role_name } : Has correct permission boundary" )
395+ else :
396+ logger .error (f"❌ { role_name } : Has wrong permission boundary: { actual_boundary } " )
397+ failed_roles .append (role_name )
398+ else :
399+ logger .error (f"❌ { role_name } : Missing permission boundary" )
400+ failed_roles .append (role_name )
401+
402+ except ClientError as e :
403+ logger .error (f"Error checking role { role_name } : { e } " )
404+ failed_roles .append (role_name )
405+
406+ if failed_roles :
407+ logger .error (f"FAILED: { len (failed_roles )} roles do not have the correct permission boundary" )
408+ return False
409+ else :
410+ logger .info (f"SUCCESS: All { len (roles )} roles have the correct permission boundary" )
411+ return True
412+
413+ except ClientError as e :
414+ logger .error (f"Error validating permission boundary: { e } " )
415+ return False
416+
310417 def install (self , admin_email : str , idp_pattern : str ):
311418 """
312- Install the IDP stack using CloudFormation with service role.
419+ Install the IDP stack using CloudFormation with service role and permission boundary .
313420
314421 Args:
315422 admin_email: Email address for the admin user
@@ -319,22 +426,30 @@ def install(self, admin_email: str, idp_pattern: str):
319426 s3_prefix = f"{ self .cfn_prefix } /0.2.2" # TODO: Make version configurable
320427
321428 try :
322- # Step 1: Ensure CloudFormation service role exists
323- logger .info ("Step 1: Ensuring CloudFormation service role exists..." )
429+ # Step 1: Create permission boundary policy
430+ logger .info ("Step 1: Creating permission boundary policy..." )
431+ permission_boundary_arn = self .create_permission_boundary_policy ()
432+ if not permission_boundary_arn :
433+ logger .error ("Failed to create permission boundary policy. Aborting deployment." )
434+ return False
435+
436+ # Step 2: Ensure CloudFormation service role exists
437+ logger .info ("Step 2: Ensuring CloudFormation service role exists..." )
324438 service_role_arn = self .deploy_service_role ()
325439 if not service_role_arn :
326440 logger .error ("Failed to deploy or find service role. Aborting IDP deployment." )
327441 return False
328442
329- # Step 2 : Deploy IDP stack using the service role
330- logger .info ("Step 2 : Deploying IDP stack using service role..." )
443+ # Step 3 : Deploy IDP stack using the service role and permission boundary
444+ logger .info ("Step 3 : Deploying IDP stack using service role and permission boundary ..." )
331445
332446 # Verify template file exists
333447 template_path = os .path .join (self .abs_cwd , template_file )
334448 if not os .path .exists (template_path ):
335449 raise FileNotFoundError (f"Template file not found: { template_path } " )
336450
337- # Construct the CloudFormation deploy command with service role
451+ logger .info (f"Using permission boundary ARN: { permission_boundary_arn } " )
452+
338453 cmd = [
339454 'aws' , 'cloudformation' , 'deploy' ,
340455 '--region' , self .region ,
@@ -347,6 +462,7 @@ def install(self, admin_email: str, idp_pattern: str):
347462 "DocumentKnowledgeBase=DISABLED" ,
348463 f"IDPPattern={ idp_pattern } " ,
349464 f"AdminEmail={ admin_email } " ,
465+ f"PermissionsBoundaryArn={ permission_boundary_arn } " ,
350466 '--stack-name' , self .stack_name
351467 ]
352468
@@ -371,7 +487,15 @@ def install(self, admin_email: str, idp_pattern: str):
371487 if process .stderr :
372488 logger .debug (f"CloudFormation deploy stderr: { process .stderr } " )
373489
374- logger .info (f"Successfully deployed stack { self .stack_name } in { self .region } using service role" )
490+ logger .info (f"Successfully deployed stack { self .stack_name } in { self .region } " )
491+
492+ # Step 4: Validate permission boundary on all roles
493+ logger .info ("Step 4: Validating permission boundary on all IAM roles..." )
494+ if not self .validate_permission_boundary (self .stack_name , permission_boundary_arn ):
495+ logger .error ("Permission boundary validation failed!" )
496+ return False
497+
498+ logger .info ("Deployment and validation completed successfully!" )
375499 return True
376500
377501 except FileNotFoundError as e :
0 commit comments