AWSTemplateFormatVersion: 2010-09-09
Description: >-
  This template deploys the resources required for the AWS blog post - How to retain resource tags in AWS Backup restore jobs.
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Tag Replicator Configuration
        Parameters: 
            - pStackBinaryURL
            - pTagsToExcludeInCopy
    ParameterLabels:
      pStackBinaryURL:
        default: The URL for the StackBinary Zip File
      pTagsToExcludeInCopy:
       default: The Comma separated list of tags to exclude from copy operation        
Parameters:
  pStackBinaryURL:
    Description: The URL for the StackBinary Zip File
    Type: String    
    Default: 'https://observer-solution-artefacts.s3.us-west-2.amazonaws.com/tagreplicator.zip'

  pTagsToExcludeInCopy:
    Description: The Comma separated list of tags to exclude from copy operation
    Type: String    
    Default: 'aws:backup:source-resource'


Resources:
  rLocalCacheBucket:
    Type: "AWS::S3::Bucket"
    DeletionPolicy: Delete
    UpdateReplacePolicy: Retain
  
  rCleanupLocalCacheBucketOnDelete:
    Type: Custom::CleanupBucket
    Properties:
      ServiceToken: !GetAtt rGlobalCfnCodeReplicatorLambda.Arn
      S3BucketToCleanup: !Ref rLocalCacheBucket  
  
  rCopySolutionToLocalCacheBucket:
    Type: Custom::ReplicateSolutionBinaries
    Properties:
      ServiceToken: !GetAtt rGlobalCfnCodeReplicatorLambda.Arn
      SolutionDestinationBucket: !Ref rLocalCacheBucket
      SolutionURL: !Ref pStackBinaryURL

  rGlobalCfnCodeReplicatorLambda:
    Type: AWS::Lambda::Function
    Metadata:
        cfn_nag:
          rules_to_suppress:
            - id: W89
              reason: "Custom resource deployed in default VPC"
            - id: W92
              reason: "ReservedConcurrentExecutions not needed since this function runs once when CloudFormation deploys"    
    Properties:
      Code:
        ZipFile: |-
          #!/usr/bin/env python
          # -*- coding: utf-8 -*-
          import json
          import boto3
          import urllib3
          import os
          import shutil
          from urllib.parse import urlparse
          physical_resource_id = 'GlobalCfnCodeReplicator'  
          def process_bucket_cleanup_request(bucket_name):
              print(f"process_bucket_cleanup_request starting for bucket_name : {bucket_name}")
              s3 = boto3.resource('s3')
              bucket_to_delete = s3.Bucket(bucket_name)
              response = bucket_to_delete.objects.all().delete()
              print(f"process_bucket_cleanup_request all object delete done. Response : {response}")
        
          def download_url(url, save_path):
            c = urllib3.PoolManager()
            with c.request('GET',url, preload_content=False) as resp, open(save_path, 'wb') as out_file:
                shutil.copyfileobj(resp, out_file)
            resp.release_conn()
            
          def lambda_handler(event, context):
            try:
                print(f'Handling event : {event}')
                request_type = event.get('RequestType')              
                solution_url = event['ResourceProperties'].get('SolutionURL')
                solution_bucket = event['ResourceProperties'].get('SolutionDestinationBucket')
                response_data = {
                    'RequestType': request_type,
                    'SolutionURL' : solution_url,
                    'SolutionDestinationBucket' : solution_bucket
                }
                if request_type == 'Create' or request_type == 'Update':
                    if solution_url:
                        print(f'downloading file from : {solution_url}')
                        a = urlparse(solution_url)
                        original_file_name = os.path.basename(a.path)
                        temp_file_name = '/tmp/'+original_file_name
                        download_url(solution_url,temp_file_name)
                        file_size = (os.stat(temp_file_name).st_size / 1024)
                        print(f'Downloaded report to File : {temp_file_name} , Size : {file_size}')
                        #Upload this to the Bucket
                        s3_client = boto3.client('s3')
                        print(f"uploading payload to : {solution_bucket} at {original_file_name}")
                        extraArgsForUpload = {'ACL':'bucket-owner-full-control', 'Tagging':'Source=StackBinaryURL'}
                        s3_client.upload_file(Filename=temp_file_name, Bucket=solution_bucket, Key=original_file_name,ExtraArgs=extraArgsForUpload)
                elif request_type == 'Delete':
                    solution_bucket = event['ResourceProperties'].get('S3BucketToCleanup')
                    if solution_bucket:
                        process_bucket_cleanup_request(solution_bucket)
                    
                send(event, context, 'SUCCESS', response_data, physical_resource_id)
            except Exception as e:
                print(f'{e}')
                send(event, context, 'FAILED', response_data, physical_resource_id)
          def send(event, context, response_status, response_data, physical_resource_id, no_echo=False):
            http = urllib3.PoolManager()
            response_url = event['ResponseURL']
            json_response_body = json.dumps({
                'Status': response_status,
                'Reason': f'See the details in CloudWatch Log Stream: {context.log_stream_name}',
                'PhysicalResourceId': physical_resource_id,
                'StackId': event['StackId'],
                'RequestId': event['RequestId'],
                'LogicalResourceId': event['LogicalResourceId'],
                'NoEcho': no_echo,
                'Data': response_data
            }).encode('utf-8')
            headers = {
                'content-type': '',
                'content-length': str(len(json_response_body))
            }
            try:
                http.request('PUT', response_url,
                             body=json_response_body, headers=headers)
            except Exception as e:  # pylint: disable = W0703
                print(e)
      Description: Copy Solutions Binary to Local Cache Bucket
      Handler: index.lambda_handler
      Role : !GetAtt rTagReplicatorAWSBackupRole.Arn
      Runtime: python3.7
      Timeout: 300

  rAWSBackupEventBridgeRule: 
    Type: AWS::Events::Rule
    Properties: 
      Description: EventBridge rule to match AWS Backup events to trigger tag replicator Lambda function.
      State: "ENABLED"
      EventPattern: 
        source:
          - 'aws.backup'
        detail-type:
          - 'Restore Job State Change'
        detail:
          status: 
              - 'COMPLETED'
      Targets: 
        - Arn: !GetAtt rTagReplicatorForAWSBackupLambda.Arn
          Id: "ProcessAWSBackupEventsUsingLambda" 

  rAWSBackupEventBridgeRuleInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref rTagReplicatorForAWSBackupLambda
      Principal: events.amazonaws.com
      SourceArn: !Sub ${rAWSBackupEventBridgeRule.Arn} 
          
 
  rTagReplicatorForAWSBackupLambda:
    DependsOn: rCopySolutionToLocalCacheBucket
    Type: AWS::Lambda::Function
    Properties:
      Description: Function to manage AWS Backup events and trigger replicator code
      FunctionName: TagReplicatorFunctionForAWSBackup
      Handler: lambda_handler.handler
      Role: !GetAtt rTagReplicatorAWSBackupRole.Arn
      Runtime: python3.7
      MemorySize: 1024
      Timeout: 600
      Code:
        S3Bucket: !Ref rLocalCacheBucket        
        S3Key: 'tagreplicator.zip'        
      Environment:
        Variables:
          TagReplicatorStackId: !Ref AWS::StackId
          TagReplicatorTagsToExclude: !Ref pTagsToExcludeInCopy
  rAWSBackupServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: Allow
            Principal:
              Service:
                - backup.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - !Sub 'arn:${AWS::Partition}:iam::${AWS::Partition}:policy/service-role/AWSBackupServiceRolePolicyForBackup'
        - !Sub 'arn:${AWS::Partition}:iam::${AWS::Partition}:policy/service-role/AWSBackupServiceRolePolicyForRestores'
        - !Sub 'arn:${AWS::Partition}:iam::${AWS::Partition}:policy/AWSBackupOperatorAccess'
        
  rTagReplicatorAWSBackupRole:
    Type: AWS::IAM::Role
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W11
            reason: IAM role should not allow * resource on its permissions policy
          - id: F3
            reason: IAM role should not allow * resource on its permissions policy
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: 'sts:AssumeRole'
      Path: /
      ManagedPolicyArns:
          - !Sub 'arn:${AWS::Partition}:iam::${AWS::Partition}:policy/service-role/AWSBackupServiceRolePolicyForBackup'
          - !Sub 'arn:${AWS::Partition}:iam::${AWS::Partition}:policy/service-role/AWSBackupServiceRolePolicyForRestores'
          - !Sub 'arn:${AWS::Partition}:iam::${AWS::Partition}:policy/service-role/AWSLambdaBasicExecutionRole'
          - !Sub 'arn:${AWS::Partition}:iam::${AWS::Partition}:policy/AWSBackupOperatorAccess'
      Policies:
          - PolicyName: invokeLambda
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
              - Effect: Allow
                Action:
                - lambda:InvokeFunction
                Resource: '*'           
          - PolicyName: AllowUserToPassAWSBackupServiceRole
            PolicyDocument:
              Version: 2012-10-17
              Statement:
                - Sid: AllowUserToPassAWSBackupServiceRole
                  Effect: Allow
                  Action: 'iam:PassRole'
                  Resource: !GetAtt rAWSBackupServiceRole.Arn
          - PolicyName: resourceTaggingRole
            PolicyDocument:
              Version: 2012-10-17
              Statement:
                - Effect: Allow
                  Action:
                    - ec2:DescribeTags
                    - ec2:CreateTags
                    - rds:ListTagsForResource
                    - rds:AddTagsToResource
                    - dynamodb:ListTagsOfResource
                    - dynamodb:TagResource
                    - elasticfilesystem:ListTagsForResource
                    - elasticfilesystem:CreateTags
                    - fsx:ListTagsForResource
                    - fsx:TagResource             
                    - storagegateway:ListTagsForResource
                    - storagegateway:AddTagsToResource
                    - neptune:ListTagsForResource
                    - neptune:AddTagsToResource
                    - rds:ListTagsForResource
                    - rds:AddTagsToResource 
                    - s3:GetBucketTagging                    
                    - s3:PutBucketTagging
                  Resource: '*'
          - PolicyName: logStreamPermissions
            PolicyDocument:
              Statement:                       
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !Sub 'arn:${AWS::Partition}:logs:*:*:*'    
          - PolicyName: s3LogBucketPermissions
            PolicyDocument:
              Statement:
              - Effect: Allow
                Action:
                  - kms:GenerateDataKey
                  - kms:Decrypt
                  - kms:Encrypt                  
                  - s3:PutObject*
                  - s3:GetObject*
                  - s3:DeleteObject
                  - s3:GetBucketLocation
                  - s3:ListBucket
                  - s3:DeleteBucket
                  - s3:ListBucketMultipartUploads
                  - s3:ListMultipartUploadParts
                  - s3:AbortMultipartUpload                  
                Resource: 
                  - !Sub 'arn:${AWS::Partition}:s3:::${rLocalCacheBucket}/*'
                  - !Sub 'arn:${AWS::Partition}:s3:::${rLocalCacheBucket}'                 
Outputs:
  oStackName:
    Value: !Ref AWS::StackName    