Transform: AWS::Serverless-2016-10-31

Parameters:
  Stage:
    Description: Stage name
    Type: String
    AllowedValues:
      - PROD
      - CODE
    Default: CODE
  StackName:
    Type: String
    Default: emails-transporter-between-sf-and-s3

Mappings:
  StageMap:
    PROD:
      Schedule: 'cron(00,30 03 * * ? *)'
      SalesforceUsername: EmailsToS3APIUser
      AppName: SFEmailsToS3
      ToLowerCase: prod
    CODE:
      Schedule: 'cron(00,30 03 * * ? *)'
      SalesforceUsername: EmailsToS3APIUser
      AppName: AwsConnectorSandbox
      ToLowerCase: code

Conditions:
  IsProd: !Equals [ !Ref Stage, PROD ]

Resources:
  IAMRoleS3ReadWrite:
    Type: "AWS::IAM::Role"
    Properties:
      Path: "/"
      RoleName: !Sub emails-to-sf-api-${Stage}-api-gateway-role
      AssumeRolePolicyDocument: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"apigateway.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
      MaxSessionDuration: 3600
      ManagedPolicyArns:
        - !Ref IAMManagedPolicy
        - "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
      Description: "Allows API Gateway to Read/Write/Delete from S3."

  IAMManagedPolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      ManagedPolicyName: !Sub "s3-emails-from-sf-${Stage}-get-delete"
      Path: "/"
      PolicyDocument: !Sub
        - "{\"Version\": \"2012-10-17\",\"Statement\": [{\"Effect\": \"Allow\",\"Action\": [\"s3:GetObject\",\"s3:PutObject\",\"s3:DeleteObject\"],\"Resource\": \"arn:aws:s3:::emails-from-sf-${lowerCaseStage}/*\"}]}"
        - { lowerCaseStage: !FindInMap [ StageMap, !Ref Stage, ToLowerCase ] }

  ApiGatewayRestApi:
    Type: "AWS::ApiGateway::RestApi"
    Properties:
      Name: !Sub import-emails-from-s3-to-sf-${Stage}-api-gateway
      ApiKeySourceType: "HEADER"
      EndpointConfiguration:
        Types:
          - "EDGE"

  ApiGatewayResourceS3Bucket:
    Type: "AWS::ApiGateway::Resource"
    Properties:
      RestApiId: !Ref ApiGatewayRestApi
      PathPart: "{bucketName}"
      ParentId: !GetAtt ApiGatewayRestApi.RootResourceId

  ApiGatewayResourceS3File:
    Type: "AWS::ApiGateway::Resource"
    Properties:
      RestApiId: !Ref ApiGatewayRestApi
      PathPart: "{caseNumber}"
      ParentId: !Ref ApiGatewayResourceS3Bucket

  ApiGatewayApiKey:
    Type: "AWS::ApiGateway::ApiKey"
    Properties:
      Enabled: true
      Name: !Sub import-emails-from-s3-to-sf-${Stage}-api-key

  ApiGatewayStage:
    Type: "AWS::ApiGateway::Stage"
    Properties:
      StageName: !Sub ${Stage}
      DeploymentId: !Ref ApiGatewayDeployment
      RestApiId: !Ref ApiGatewayRestApi
      CacheClusterEnabled: false
      MethodSettings:
        - CacheDataEncrypted: false
          CacheTtlInSeconds: 300
          CachingEnabled: false
          DataTraceEnabled: true
          HttpMethod: "*"
          LoggingLevel: "INFO"
          MetricsEnabled: false
          ResourcePath: "/*"
          ThrottlingBurstLimit: 5000
          ThrottlingRateLimit: 10000
      TracingEnabled: false

  ApiGatewayMethodGET:
    Type: "AWS::ApiGateway::Method"
    DependsOn:
      - IAMRoleS3ReadWrite
    Properties:
      RestApiId: !Ref ApiGatewayRestApi
      ResourceId: !Ref ApiGatewayResourceS3File
      HttpMethod: "GET"
      AuthorizationType: "NONE"
      ApiKeyRequired: true
      RequestParameters:
        "method.request.path.bucketName": true
        "method.request.path.caseNumber": true
      MethodResponses:
        - ResponseParameters:
            "method.response.header.Content-Length": false
            "method.response.header.Content-Type": false
            "method.response.header.Timestamp": false
          StatusCode: "200"
        - ResponseParameters:
            "method.response.header.Content-Length": false
            "method.response.header.Content-Type": false
            "method.response.header.Timestamp": false
          StatusCode: "400"
        - ResponseParameters:
            "method.response.header.Content-Length": false
            "method.response.header.Content-Type": false
            "method.response.header.Timestamp": false
          StatusCode: "500"
      Integration:
        CacheNamespace: !Ref ApiGatewayResourceS3File
        Credentials: !Sub "arn:aws:iam::${AWS::AccountId}:role/emails-to-sf-api-${Stage}-api-gateway-role"
        IntegrationHttpMethod: "GET"
        IntegrationResponses:
          - ResponseParameters:
              "method.response.header.Content-Length": "integration.response.header.Content-Length"
              "method.response.header.Content-Type": "integration.response.header.Content-Type"
              "method.response.header.Timestamp": "integration.response.header.Date"
            ResponseTemplates: { }
            SelectionPattern: "2\\d{2}"
            StatusCode: "200"
          - SelectionPattern: "4\\d{2}"
            StatusCode: "400"
          - SelectionPattern: "5\\d{2}"
            StatusCode: "500"
        PassthroughBehavior: "WHEN_NO_MATCH"
        RequestParameters:
          "integration.request.path.folder": "method.request.path.bucketName"
          "integration.request.path.item": "method.request.path.caseNumber"
        TimeoutInMillis: 29000
        Type: "AWS"
        Uri: !Sub "arn:aws:apigateway:${AWS::Region}:s3:path/{folder}/{item}"

  ApiGatewayMethodPOST:
    Type: "AWS::ApiGateway::Method"
    DependsOn:
      - IAMRoleS3ReadWrite
    Properties:
      RestApiId: !Ref ApiGatewayRestApi
      ResourceId: !Ref ApiGatewayResourceS3Bucket
      HttpMethod: "POST"
      AuthorizationType: "NONE"
      ApiKeyRequired: true
      RequestParameters:
        "method.request.header.x-amz-checksum-sha1": false
        "method.request.header.x-amz-sdk-checksum-algorithm": false
        "method.request.path.bucketName": true
      MethodResponses:
        - ResponseParameters:
            "method.response.header.Content-Length": false
            "method.response.header.Content-Type": false
            "method.response.header.Timestamp": false
          StatusCode: "200"
        - ResponseParameters:
            "method.response.header.Content-Length": false
            "method.response.header.Content-Type": false
            "method.response.header.Timestamp": false
          StatusCode: "400"
        - ResponseParameters:
            "method.response.header.Content-Length": false
            "method.response.header.Content-Type": false
            "method.response.header.Timestamp": false
          StatusCode: "500"
      Integration:
        CacheNamespace: !Ref ApiGatewayResourceS3File
        Credentials: !Sub "arn:aws:iam::${AWS::AccountId}:role/emails-to-sf-api-${Stage}-api-gateway-role"
        IntegrationHttpMethod: "POST"
        IntegrationResponses:
          - ResponseParameters:
              "method.response.header.Content-Length": "integration.response.header.Content-Length"
              "method.response.header.Content-Type": "integration.response.header.Content-Type"
              "method.response.header.Timestamp": "integration.response.header.Date"
            ResponseTemplates: { }
            SelectionPattern: "2\\d{2}"
            StatusCode: "200"
          - SelectionPattern: "4\\d{2}"
            StatusCode: "400"
          - SelectionPattern: "5\\d{2}"
            StatusCode: "500"
        PassthroughBehavior: "WHEN_NO_MATCH"
        RequestParameters:
          "integration.request.header.x-amz-checksum-sha1": "method.request.header.x-amz-checksum-sha1"
          "integration.request.header.x-amz-sdk-checksum-algorithm": "method.request.header.x-amz-sdk-checksum-algorithm"
        TimeoutInMillis: 29000
        Type: "AWS"
        Uri:
          !Sub
            - "arn:aws:apigateway:${AWS::Region}:emails-from-sf-${lowerCaseStage}.s3:path/?delete"
            - lowerCaseStage: !FindInMap [ StageMap, !Ref Stage, ToLowerCase ]

  ApiGatewayDeployment:
    Type: "AWS::ApiGateway::Deployment"
    DependsOn:
      - ApiGatewayMethodGET
      - ApiGatewayMethodPOST
    Properties:
      RestApiId: !Ref ApiGatewayRestApi

  ApiGatewayUsagePlan:
    Type: "AWS::ApiGateway::UsagePlan"
    DependsOn:
      - ApiGatewayStage
    Properties:
      UsagePlanName: !Sub import-emails-from-s3-to-sf-${Stage}-usage-plan
      ApiStages:
        - ApiId: !Ref ApiGatewayRestApi
          Stage: !Sub ${Stage}

  ApiGatewayUsagePlanKey:
    Type: "AWS::ApiGateway::UsagePlanKey"
    DependsOn:
      - ApiGatewayUsagePlan
      - ApiGatewayStage
    Properties:
      KeyId: !GetAtt ApiGatewayApiKey.APIKeyId
      KeyType: "API_KEY"
      UsagePlanId: !Ref ApiGatewayUsagePlan

  LambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      Description: Retrieves emails from Salesforce and saves as Json to S3
      FunctionName: !Sub export-emails-from-sf-to-s3-${Stage}
      Handler: com.gu.sf_emails_to_s3_exporter.Handler::handleRequest
      CodeUri:
        Bucket: support-service-lambdas-dist
        Key: !Sub membership/${Stage}/sf-emails-to-s3-exporter/sf-emails-to-s3-exporter.jar
      MemorySize: 512
      Runtime: java21
      Timeout: 900
      Environment:
        Variables:
          Stage: !Ref Stage
          bucketName: !Sub emails-from-sf-${Stage}
          sfApiVersion: v50.0
      Policies:
        - Statement:
          - Effect: Allow
            Action:
              - secretsmanager:DescribeSecret
              - secretsmanager:GetSecretValue
            Resource:
              - "arn:aws:secretsmanager:eu-west-1:865473395570:secret:CODE/Salesforce/ConnectedApp/AwsConnectorSandbox-jaCgRl"
              - "arn:aws:secretsmanager:eu-west-1:865473395570:secret:PROD/Salesforce/ConnectedApp/SFEmailsToS3-6QJGTX"
              - "arn:aws:secretsmanager:eu-west-1:865473395570:secret:CODE/Salesforce/User/EmailsToS3APIUser-EbXFEb"
              - "arn:aws:secretsmanager:eu-west-1:865473395570:secret:PROD/Salesforce/User/EmailsToS3APIUser-kGtUDC"
        - Statement:
          - Effect: Allow
            Action: cloudwatch:PutMetricData
            Resource: "*"
        - Statement:
          - Effect: Allow
            Action: s3:GetObject
            Resource:
              - !Sub
                - "arn:aws:s3:::emails-from-sf-${lowerCaseStage}/*"
                - { lowerCaseStage: !FindInMap [ StageMap, !Ref Stage, ToLowerCase ] }
        - Statement:
          - Effect: Allow
            Action: s3:PutObject
            Resource:
              - !Sub
                - "arn:aws:s3:::emails-from-sf-${lowerCaseStage}/*"
                - { lowerCaseStage: !FindInMap [ StageMap, !Ref Stage, ToLowerCase ] }
        - Statement:
          - Effect: Allow
            Action: s3:ListBucket
            Resource:
              - !Sub
                - "arn:aws:s3:::emails-from-sf-${lowerCaseStage}"
                - { lowerCaseStage: !FindInMap [ StageMap, !Ref Stage, ToLowerCase ] }
              - !Sub
                - "arn:aws:s3:::emails-from-sf-${lowerCaseStage}/*"
                - { lowerCaseStage: !FindInMap [ StageMap, !Ref Stage, ToLowerCase ] }
        - Statement:
          - Effect: Allow
            Action: s3:ListObjectV2
            Resource:
              - !Sub
                - "arn:aws:s3:::emails-from-sf-${lowerCaseStage}"
                - { lowerCaseStage: !FindInMap [ StageMap, !Ref Stage, ToLowerCase ] }
              - !Sub
                - "arn:aws:s3:::emails-from-sf-${lowerCaseStage}/*"
                - { lowerCaseStage: !FindInMap [ StageMap, !Ref Stage, ToLowerCase ] }
        - Statement:
          - Sid: readDeployedArtefact
            Effect: Allow
            Action: s3:GetObject
            Resource:
              - arn:aws:s3::*:membership-dist/*
      Events:
        ScheduledRun:
          Type: Schedule
          Properties:
            Schedule: !FindInMap [ StageMap, !Ref Stage, Schedule ]
            Description: Runs SF Emails to S3 Exporter
            Enabled: True

  failedS3WriteFileAlarm:
    Type: AWS::CloudWatch::Alarm
    Condition: IsProd
    DependsOn:
      - LambdaFunction
    Properties:
      AlarmActions:
        - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:alarms-handler-topic-PROD
      AlarmName: emails-from-sf failed when writing file to s3
      AlarmDescription: >
        !Sub Something went wrong when writing a file to S3 bucket emails-from-sf-${Stage}
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: "Stage"
          Value: !Ref Stage
      DatapointsToAlarm: 1
      EvaluationPeriods: 1
      MetricName: failed_s3_write_file
      Namespace: s3-emails-from-sf
      Period: 3600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching

  failedS3CheckFileExistsAlarm:
    Type: AWS::CloudWatch::Alarm
    Condition: IsProd
    DependsOn:
      - LambdaFunction
    Properties:
      AlarmActions:
        - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:alarms-handler-topic-PROD
      AlarmName: emails-from-sf failed when checking if a file exists in s3
      AlarmDescription: >
        !Sub Something went wrong when checking if a file exists in S3 bucket emails-from-sf-${Stage}
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: "Stage"
          Value: !Ref Stage
      DatapointsToAlarm: 1
      EvaluationPeriods: 1
      MetricName: failed_s3_check_file_exists
      Namespace: s3-emails-from-sf
      Period: 3600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching

  failedS3GetFileAlarm:
    Type: AWS::CloudWatch::Alarm
    Condition: IsProd
    DependsOn:
      - LambdaFunction
    Properties:
      AlarmActions:
        - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:alarms-handler-topic-PROD
      AlarmName: emails-from-sf failed when getting file from s3
      AlarmDescription: >
        !Sub Something went wrong when getting file from S3 bucket emails-from-sf-${Stage}
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: "Stage"
          Value: !Ref Stage
      DatapointsToAlarm: 1
      EvaluationPeriods: 1
      MetricName: failed_s3_get_file
      Namespace: s3-emails-from-sf
      Period: 3600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching

  failedWritebackToSFRequestAlarm:
    Type: AWS::CloudWatch::Alarm
    Condition: IsProd
    DependsOn:
      - LambdaFunction
    Properties:
      AlarmActions:
        - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:alarms-handler-topic-PROD
      AlarmName: emails-from-sf failed request to Salesforce
      AlarmDescription: >
        !Sub Something went wrong writing successes back to Salesforce (Bad Request)
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: "Stage"
          Value: !Ref Stage
      DatapointsToAlarm: 1
      EvaluationPeriods: 1
      MetricName: failed_writeback_request_to_sf
      Namespace: s3-emails-from-sf
      Period: 3600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching

  failedWritebackToSFRecordAlarm:
    Type: AWS::CloudWatch::Alarm
    Condition: IsProd
    DependsOn:
      - LambdaFunction
    Properties:
      AlarmActions:
        - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:alarms-handler-topic-PROD
      AlarmName: emails-from-sf failed writeback to Salesforce record
      AlarmDescription: >
        Something went wrong writing success back to Salesforce record
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: "Stage"
          Value: !Ref Stage
      DatapointsToAlarm: 1
      EvaluationPeriods: 1
      MetricName: failed_writeback_to_sf_record
      Namespace: s3-emails-from-sf
      Period: 3600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching

  failedToRetrieveEmailsFromSalesforceAlarm:
    Type: AWS::CloudWatch::Alarm
    Condition: IsProd
    DependsOn:
      - LambdaFunction
    Properties:
      AlarmActions:
        - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:alarms-handler-topic-PROD
      AlarmName: emails-from-sf failed to retrieve emails from Salesforce
      AlarmDescription: >
        Something went wrong retrieving emails from Salesforce
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: "Stage"
          Value: !Ref Stage
      DatapointsToAlarm: 1
      EvaluationPeriods: 1
      MetricName: failed_to_get_records_from_sf
      Namespace: s3-emails-from-sf
      Period: 3600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching

  failedToAuthenticateWithSalesforceAlarm:
    Type: AWS::CloudWatch::Alarm
    Condition: IsProd
    DependsOn:
      - LambdaFunction
    Properties:
      AlarmActions:
        - !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:alarms-handler-topic-PROD
      AlarmName: emails-from-sf failed to authenticate with Salesforce
      AlarmDescription: >
        Something went wrong authenticating with Salesforce
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: "Stage"
          Value: !Ref Stage
      DatapointsToAlarm: 1
      EvaluationPeriods: 1
      MetricName: failed_to_authenticate_with_sf
      Namespace: s3-emails-from-sf
      Period: 3600
      Statistic: Sum
      Threshold: 1
      TreatMissingData: notBreaching
