def prepare_s3bucket_artifact(self, is_zip=False): """ Add a Hook which will prepare the Lambda Code artifact and upload to an S3 Bucket if it doesn't already exist. Add Parameters for CodeS3Bucket and CodeS3Key. The Parameter values are placeholder values that will be updated by the hook. """ ignore_changes = True # always create an artifact on CREATE stack_hooks = StackHooks() stack_hooks.add( name='PrepLambdaCodeArtifactToS3Bucket', stack_action=['create',], stack_timing='pre', hook_method=self.prepare_s3bucket_artifact_hook, cache_method=self.prepare_s3bucket_artifact_cache, hook_arg=is_zip, ) self.stack.add_hooks(stack_hooks) # only UPDATE an artifact if --auto-publish-code is true if self.paco_ctx.auto_publish_code == True: ignore_changes = False stack_hooks = StackHooks() stack_hooks.add( name='PrepLambdaCodeArtifactToS3Bucket', stack_action=['update'], stack_timing='pre', hook_method=self.prepare_s3bucket_artifact_hook, cache_method=self.prepare_s3bucket_artifact_cache, hook_arg=is_zip, ) self.stack.add_hooks(stack_hooks) self.s3bucket_param = self.create_cfn_parameter( name='CodeS3Bucket', description="S3 Bucket for the Lambda Code artifact", param_type='String', value='', ignore_changes=ignore_changes, ) self.s3key_param = self.create_cfn_parameter( name='CodeS3Key', description="S3 Key for the Lambda Code artifact", param_type='String', value='', ignore_changes=ignore_changes, ) cfn_s3_code = { 'S3Bucket': troposphere.Ref(self.s3bucket_param), 'S3Key': troposphere.Ref(self.s3key_param), } return cfn_s3_code
def init_resource(self): stack_hooks = StackHooks() # Stack hooks for saving state to the Paco bucket stack_hooks.add( name='EventsRule.State', stack_action=['create', 'update'], stack_timing='post', hook_method=self.stack_hook_eventsrule_state, cache_method=self.stack_hook_eventsrule_state_cache_id, ) # CloudWatch Events Rule self.stack_group.add_new_stack(self.aws_region, self.resource, paco.cftemplates.eventsrule.EventsRule, account_ctx=self.account_ctx, stack_tags=self.stack_tags, stack_hooks=stack_hooks)
def init(self): stack_hooks = StackHooks() for hook_action in ['create', 'update']: stack_hooks.add( name='CodeCommitSSHPublicKey', stack_action=hook_action, stack_timing='post', hook_method=self.codecommit_post_stack_hook, cache_method=self.codecommit_post_stack_hook_cache_id, hook_arg=self.config) # CodeCommit Repository codecommit_stack = self.add_new_stack( self.aws_region, self.config, paco.cftemplates.CodeCommit, extra_context={'repo_list': self.repo_list}, stack_hooks=stack_hooks) codecommit_stack.set_termination_protection(True) self.stack_list.append(codecommit_stack)
def add_bucket(self, bucket, bucket_name_prefix=None, bucket_name_suffix=None, stack_hooks=None, change_protected=False): "Add a bucket: will create a stack and stack hooks as needed" if self.bucket_context['config'] != None: raise PacoBucketExists("Bucket already exists: %s" % (self.resource_ref)) bucket.bucket_name_prefix = bucket_name_prefix bucket.bucket_name_suffix = bucket_name_suffix res_group = get_parent_by_interface(bucket, schemas.IResourceGroup) if res_group != None: self.bucket_context['group_id'] = res_group.name self.bucket_context['config'] = bucket self.bucket_context['ref'] = self.resource_ref bucket.resolve_ref_obj = self if bucket.external_resource == True: # if the bucket already exists, do not create a stack for it pass else: if change_protected == False: if stack_hooks == None: stack_hooks = StackHooks() # S3 Delete on Stack Delete hook stack_hooks.add('S3StackGroup', 'delete', 'pre', self.stack_hook_pre_delete, None, self.bucket_context) self.add_stack( bucket_policy_only=False, stack_hooks=stack_hooks, stack_tags=self.stack_tags, )
def __init__(self, stack, paco_ctx): cup = stack.resource super().__init__(stack, paco_ctx, iam_capabilities=["CAPABILITY_IAM"]) self.set_aws_name('CUP', self.resource_group_name, self.resource.name) self.init_template('Cognito User Pool') if not cup.is_enabled(): return cfn_export_dict = cup.cfn_export_dict # SNS Role for SMS if cup.mfa != 'off': # CloudFormation requires an SMS Role even if only software tokens are used sms_role_resource = troposphere.iam.Role( 'CognitoSMSRole', AssumeRolePolicyDocument=PolicyDocument( Statement=[ Statement( Effect=Allow, Principal=Principal('Service',"cognito-idp.amazonaws.com"), Action=[Action('sts', 'AssumeRole')], Condition=Condition([ StringEquals({"sts:ExternalId": cup.paco_ref_parts}), ]), ), ], ), Policies=[ troposphere.iam.Policy( PolicyName="AllowSMS", PolicyDocument=Policy( Version='2012-10-17', Statement=[ Statement( Effect=Allow, Action=[awacs.sns.Publish], Resource=['*'], ) ] ) ) ], ) self.template.add_resource(sms_role_resource) cfn_export_dict['SmsConfiguration'] = { 'ExternalId': cup.paco_ref_parts, 'SnsCallerArn': troposphere.GetAtt(sms_role_resource, "Arn") } # Lambda Triggers lambda_trigger_mapping = [ ('create_auth_challenge', 'CreateAuthChallenge'), ('custom_message', 'CustomMessage'), ('define_auth_challenge', 'DefineAuthChallenge'), ('post_authentication', 'PostAuthentication'), ('post_confirmation', 'PostConfirmation'), ('pre_authentication', 'PreAuthentication'), ('pre_sign_up', 'PreSignUp'), ('pre_token_generation', 'PreTokenGeneration'), ('user_migration', 'UserMigration'), ('verify_auth_challenge_response', 'VerifyAuthChallengeResponse'), ] self.lambda_trigger_params = {} if cup.lambda_triggers != None: triggers = {} for name, cfn_key in lambda_trigger_mapping: lambda_ref = getattr(cup.lambda_triggers, name, None) if lambda_ref != None: if lambda_ref not in self.lambda_trigger_params: self.lambda_trigger_params[lambda_ref] = self.create_cfn_parameter( param_type='String', name='LambdaTrigger' + md5sum(str_data=lambda_ref), description=f'LambdaTrigger for Lambda {lambda_ref}', value=lambda_ref + '.arn', ) triggers[cfn_key] = troposphere.Ref(self.lambda_trigger_params[lambda_ref]) cfn_export_dict['LambdaConfig'] = triggers # Cognito User Pool cup_resource = troposphere.cognito.UserPool.from_dict( 'CognitoUserPool', cfn_export_dict ) self.template.add_resource(cup_resource) # Add Lambda Permissions for Lambda Triggers # Need to do this after the cup_resource is created lambda_permissions = {} if cup.lambda_triggers != None: for name, cfn_key in lambda_trigger_mapping: lambda_ref = getattr(cup.lambda_triggers, name, None) if lambda_ref != None: # Lambda Permission if lambda_ref not in lambda_permissions: lambda_permissions[lambda_ref] = True troposphere.awslambda.Permission( title='LambdaPermission' + md5sum(str_data=cup.paco_ref_parts), template=self.template, Action="lambda:InvokeFunction", FunctionName=troposphere.Ref(self.lambda_trigger_params[lambda_ref]), Principal='cognito-idp.amazonaws.com', SourceArn=troposphere.GetAtt(cup_resource, "Arn"), ) # Outputs self.create_output( title=cup_resource.title + 'Id', description="Cognito UserPool Id", value=troposphere.Ref(cup_resource), ref=[cup.paco_ref_parts, cup.paco_ref_parts + ".id"], ) self.create_output( title=cup_resource.title + 'Arn', description="Cognito UserPool Arn", value=troposphere.GetAtt(cup_resource, "Arn"), ref=cup.paco_ref_parts + ".arn" ) self.create_output( title=cup_resource.title + 'ProviderName', description="Cognito UserPool ProviderName", value=troposphere.GetAtt(cup_resource, "ProviderName"), ref=[cup.paco_ref_parts + ".name", cup.paco_ref_parts + ".providername"], ) self.create_output( title=cup_resource.title + 'Url', description="Cognito UserPool ProviderURL", value=troposphere.GetAtt(cup_resource, "ProviderURL"), ref=[cup.paco_ref_parts + ".url", cup.paco_ref_parts + ".providerurl"], ) # Cognito User Pool Clients for client in cup.app_clients.values(): cfn_export_dict = client.cfn_export_dict cfn_export_dict['UserPoolId'] = troposphere.Ref(cup_resource) client_logical_id = self.create_cfn_logical_id(f"{client.name}CognitoUserPoolClient") cupclient_resource = troposphere.cognito.UserPoolClient.from_dict( client_logical_id, cfn_export_dict ) self.template.add_resource(cupclient_resource) self.create_output( title=cupclient_resource.title + 'Id', description="Cognito UserPoolClient Id", value=troposphere.Ref(cupclient_resource), ref=client.paco_ref_parts + ".id", ) if client.domain_name: # ToDo: add support for custom domains up_domain_name = self.create_cfn_logical_id(f"{client.name}UserPoolDomain") domain_resource = troposphere.cognito.UserPoolDomain( up_domain_name, Domain=client.domain_name, UserPoolId=troposphere.Ref(cup_resource) ) self.template.add_resource(domain_resource) # UI Customizations if cup.ui_customizations != None: if cup.ui_customizations.logo_file != None or cup.ui_customizations.css_file != None: # Add a Hook to set UI Customizations # CloudFormation doesn't support the Logo customization # Paco also uses the hook for CSS (this could be migration to the CloudFormation ~shrug~) stack_hooks = StackHooks() stack_hooks.add( name='SetCognitoUICustomizations', stack_action=['create','update'], stack_timing='post', hook_method=self.add_ui_customizations_hook, cache_method=self.add_ui_customizations_cache, hook_arg=cup, ) stack.add_hooks(stack_hooks)
def init_users(self, model_obj): "Initialize IAM User StackGroups" self.stack_group_filter = model_obj.paco_ref_parts master_account_ctx = self.paco_ctx.get_account_context(account_ref='paco.ref accounts.master') # StackGroup for account_name in self.paco_ctx.project['accounts'].keys(): account_ctx = self.paco_ctx.get_account_context('paco.ref accounts.'+account_name) self.iam_user_stack_groups[account_name] = IAMUserStackGroup(self.paco_ctx, account_ctx, account_name, self) stack_hooks = StackHooks() for user_name in self.iam.users.keys(): user_config = self.iam.users[user_name] # Stack hooks for managing access keys for hook_action in ['create', 'update']: stack_hooks.add( name='IAMUserAccessKeys', stack_action=hook_action, stack_timing='post', hook_method=self.iam_user_access_keys_hook, cache_method=self.iam_user_access_keys_hook_cache_id, hook_arg=user_config ) # If account_whitelist is not ['all'] then validate that there are no accounts specified # in permissions that are not part of the account_whitelist for iam_user in self.iam.users.values(): allowed = {} for account in iam_user.account_whitelist: allowed[account] = None # If all accounts are allowed, no need to do this check. if 'all' in allowed: break for permission in iam_user.permissions.values(): # Some permissions don't have accounts so we ignore them here if hasattr(permission, 'accounts') == False: continue for account in permission.accounts: if account != 'all' and account not in allowed: raise InvalidAccountPermission( f"User {iam_user.name} has permission {permission.name} for account {account} - that account is not granted in that user's account_whitelist." ) # stack for the IAM User - this only exists in the Master account # and delegate roles are provisioned in accounts self.iam_user_stack_groups['master'].add_new_stack( master_account_ctx.config.region, self.iam.users, IAMUsers, account_ctx=master_account_ctx, stack_hooks=stack_hooks, ) for user in self.iam.users.values(): # Build a list of permissions for each account permissions_by_account = {} # Initialize permission_by_account for all accounts for account_name in self.paco_ctx.project['accounts'].keys(): permissions_by_account[account_name] = [] for permission_name in user.permissions.keys(): permission_config = user.permissions[permission_name] init_method = getattr(self, "init_{}_permission".format(permission_config.type.lower())) init_method(permission_config, permissions_by_account) # Give access to accounts the user has explicit access to for account_name in self.paco_ctx.project['accounts'].keys(): account_ctx = self.paco_ctx.get_account_context('paco.ref accounts.' + account_name) # IAM User delegate stack self.iam_user_stack_groups[account_name].add_new_stack( master_account_ctx.config.region, user, IAMUserAccountDelegates, stack_orders=[StackOrder.PROVISION ,StackOrder.WAITLAST], account_ctx=account_ctx, extra_context={ 'permissions_list': permissions_by_account[account_name], 'account_id': account_ctx.id, 'master_account_id': master_account_ctx.id, } ) # Print out the SwitchRole URLs for each user for user in self.iam.users.values(): print(f'{user.description} Switch Role URLs') for account_name in self.paco_ctx.project['accounts'].keys(): if account_name not in user.account_whitelist and 'all' not in user.account_whitelist: continue account_id = self.paco_ctx.get_account_context(account_name=account_name).id print(f'{account_name.capitalize()}:\nhttps://signin.aws.amazon.com/switchrole?account={account_id}&roleName=IAM-User-Account-Delegate-Role-{user.name}') print()