#Isenguardapproved---
AWSTemplateFormatVersion: '2010-09-09'
Description: A basic template that uses AWS Lambda with an AWS Transfer Family server
  to integrate SecretsManager as an identity provider. It authenticates against an
  entry in AWS Secrets Manager of the format SFTP/username. Additionally, the secret
  must hold the key-value pairs for all user properties returned to AWS Transfer Family.
  You can also modify the AWS Lambda function code to update user access.
Parameters:
  SecretsManagerRegion:
    Type: String
    Description: (Optional) The region the secrets are stored in. If this value is not provided, the
      region this stack is deployed in will be used. Use this field if you are deploying this stack in
      a region where SecretsManager is not available.
    Default: ''
Conditions:
  SecretsManagerRegionProvided:
    Fn::Not:
      - Fn::Equals:
          - Ref: SecretsManagerRegion
          - ''
Outputs:
  ServerId:
    Value:
      Fn::GetAtt: TransferServer.ServerId
    Description: ID of the Transfer Family server
  StackArn:
    Value:
      Ref: AWS::StackId
  SnsTopicArn:
    Description: SNS topic ARN
    Value: !Ref PGPblogSNStopic
  BucketName:    
    Value: !Ref S3Bucket    
    Description: Name of the S3 bucket
  SftpIamRoleArn:
    Value: 
      Fn::GetAtt: SFTPAccesstoS3.Arn
    Description: IAM Role for Transfer Family access to S3
Resources:
# Create Transfer Family server  
  TransferServer:
    Type: AWS::Transfer::Server
    Properties:
      EndpointType: PUBLIC
      IdentityProviderDetails:
        Function:
          Fn::GetAtt: GetUserConfigLambda.Arn
      IdentityProviderType: AWS_LAMBDA
      LoggingRole:
        Fn::GetAtt: CloudWatchLoggingRole.Arn
# Create IAM role for Transfer Family CloudWatch logging  
  CloudWatchLoggingRole:
    Description: IAM role used by Transfer  to log API requests to CloudWatch
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join [ '', ['TransferLogPolicy', !Select [0, !Split [-, !Select [2, !Split [/, !Ref AWS::StackId ]]]]]]
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - transfer.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: TransferLogsPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:DescribeLogStreams
                  - logs:PutLogEvents
                Resource:
                  Fn::Sub: '*'
# Create S3 bucket to store files uploaded to AWS via Transfer Family  
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Join [ '', ['pgp-blog-s3-bucket', !Select [0, !Split [-, !Select [2, !Split [/, !Ref AWS::StackId ]]]]]]
# Create SNS topic        
  PGPblogSNStopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Join [ '', ['PGPblogSNStopic', !Select [0, !Split [-, !Select [2, !Split [/, !Ref AWS::StackId ]]]]]]
# Create IAM role for authentication Lambda execution  
  AuthenticationLambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join [ '', ['LambdaSecretsPolicy', !Select [0, !Split [-, !Select [2, !Split [/, !Ref AWS::StackId ]]]]]]
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      ManagedPolicyArns:
      - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
      - PolicyName: LambdaSecretsPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - secretsmanager:GetSecretValue
            Resource:
              Fn::Sub:
                - arn:${AWS::Partition}:secretsmanager:${SecretsRegion}:${AWS::AccountId}:secret:aws/transfer/*
                - SecretsRegion:
                    Fn::If:
                      - SecretsManagerRegionProvided
                      - Ref: SecretsManagerRegion
                      - Ref: AWS::Region
                
# Create IAM role for exception handler Lambda execution  
  ExceptionLambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join [ '', ['LambdaExceptionRole', !Select [0, !Split [-, !Select [2, !Split [/, !Ref AWS::StackId ]]]]]]
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      ManagedPolicyArns:
      - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
      - PolicyName: LambdaExceptionPolicyS3
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - s3:GetBucketLocation
            - s3:ListBucket
            - s3:GetObject
            - s3:GetObjectVersion
            - s3:PutObject
            - s3:DeleteObject
            - s3:DeleteObjectVersion
            Resource:
              - !Sub arn:aws:s3:::${S3Bucket}                
              - !Sub arn:aws:s3:::${S3Bucket}/*
      - PolicyName: LambdaExceptionPolicySNS
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - transfer:SendWorkflowStepState
            Resource:
            - Fn::Sub: arn:${AWS::Partition}:transfer:${AWS::Region}:${AWS::AccountId}:workflow/*
      - PolicyName: LambdaExceptionPolicyTransferWorkflows
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - sns:Publish
            Resource:
              - !Ref PGPblogSNStopic
# Create IAM role for Transfer Family Managed Workflow execution  
  TransferFamilyWorkflowRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join [ '', ['PGPblogWorkflow', !Select [0, !Split [-, !Select [2, !Split [/, !Ref AWS::StackId ]]]]]]
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
              - transfer.amazonaws.com
          Action: 
            - sts:AssumeRole
      Policies:
      - PolicyName: PGPblogWorkflow
        PolicyDocument: 
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
              - s3:GetBucketLocation
              - s3:GetObjectTagging
              - s3:ListBucket
              - s3:*Object
              - s3:PutObjectTagging
            Resource:
              - !GetAtt S3Bucket.Arn
              - !Sub '${S3Bucket.Arn}/*'
          - Effect: Allow
            Action:
              - lambda:InvokeFunction
            Resource: 
              !GetAtt ExceptionLambda.Arn
          - Effect: Allow
            Action: 
              - secretsmanager:GetSecretValue
            Resource:
              Fn::Sub:
                - arn:${AWS::Partition}:secretsmanager:${SecretsRegion}:${AWS::AccountId}:secret:aws/transfer/*
                - SecretsRegion:
                    Fn::If:
                      - SecretsManagerRegionProvided
                      - Ref: SecretsManagerRegion
                      - Ref: AWS::Region
# Create an IAM role to allow TransferFamily to access S3
  SFTPAccesstoS3:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join [ '', ['SftpS3Access', !Select [0, !Split [-, !Select [2, !Split [/, !Ref AWS::StackId ]]]]]]
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - transfer.amazonaws.com
          Action: 
          - sts:AssumeRole
      Policies:
      - PolicyName: AccesstoS3
        PolicyDocument: 
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - s3:GetBucketLocation
            - s3:ListBucket
            Resource: 
              !GetAtt S3Bucket.Arn
          - Effect: Allow
            Action:
            - s3:PutObject
            - s3:GetObject
            - s3:DeleteObject
            - s3:GetObjectACL
            - s3:PutObjectACL
            - s3:*Object
            - s3:PutObjectTagging
            Resource: 
              - !GetAtt S3Bucket.Arn
              - !Sub '${S3Bucket.Arn}/*'
# Create Lambda function for Transfer Family custom authentication                      
  GetUserConfigLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Join [ '', ['GetUserConfig', !Select [0, !Split [-, !Select [2, !Split [/, !Ref AWS::StackId ]]]]]]
      Code:
        ZipFile:
          Fn::Sub: |
            import os
            import json
            import boto3
            import base64
            from botocore.exceptions import ClientError

            def lambda_handler(event, context):
                resp_data = {}

                if 'username' not in event or 'serverId' not in event:
                    print("Incoming username or serverId missing  - Unexpected")
                    return response_data

                # It is recommended to verify server ID against some value, this template does not verify server ID
                input_username = event['username']
                input_serverId = event['serverId']
                print("Username: {}, ServerId: {}".format(input_username, input_serverId));

                if 'password' in event:
                    input_password = event['password']
                    if input_password == '' and (event['protocol'] == 'FTP' or event['protocol'] == 'FTPS'):
                      print("Empty password not allowed")
                      return response_data
                else:
                    print("No password, checking for SSH public key")
                    input_password = ''

                # Lookup user's secret which can contain the password or SSH public keys
                resp = get_secret("aws/transfer/" + input_serverId + "/" + input_username)

                if resp != None:
                    resp_dict = json.loads(resp)
                else:
                    print("Secrets Manager exception thrown")
                    return {}

                if input_password != '':
                    if 'Password' in resp_dict:
                        resp_password = resp_dict['Password']
                    else:
                        print("Unable to authenticate user - No field match in Secret for password")
                        return {}

                    if resp_password != input_password:
                        print("Unable to authenticate user - Incoming password does not match stored")
                        return {}
                else:
                    # SSH Public Key Auth Flow - The incoming password was empty so we are trying ssh auth and need to return the public key data if we have it
                    if 'PublicKey' in resp_dict:
                        resp_data['PublicKeys'] = resp_dict['PublicKey'].split(",")
                    else:
                        print("Unable to authenticate user - No public keys found")
                        return {}

                # If we've got this far then we've either authenticated the user by password or we're using SSH public key auth and
                # we've begun constructing the data response. Check for each key value pair.
                # These are required so set to empty string if missing
                if 'Role' in resp_dict:
                    resp_data['Role'] = resp_dict['Role']
                else:
                    print("No field match for role - Set empty string in response")
                    resp_data['Role'] = ''

                # These are optional so ignore if not present
                if 'Policy' in resp_dict:
                    resp_data['Policy'] = resp_dict['Policy']

                if 'HomeDirectoryDetails' in resp_dict:
                    print("HomeDirectoryDetails found - Applying setting for virtual folders")
                    resp_data['HomeDirectoryDetails'] = resp_dict['HomeDirectoryDetails']
                    resp_data['HomeDirectoryType'] = "LOGICAL"
                elif 'HomeDirectory' in resp_dict:
                    print("HomeDirectory found - Cannot be used with HomeDirectoryDetails")
                    resp_data['HomeDirectory'] = resp_dict['HomeDirectory']
                else:
                    print("HomeDirectory not found - Defaulting to /")

                print("Completed Response Data: "+json.dumps(resp_data))
                return resp_data

            def get_secret(id):
                region = os.environ['SecretsManagerRegion']
                print("Secrets Manager Region: "+region)

                client = boto3.session.Session().client(service_name='secretsmanager', region_name=region)

                try:
                    resp = client.get_secret_value(SecretId=id)
                    # Decrypts secret using the associated KMS CMK.
                    # Depending on whether the secret is a string or binary, one of these fields will be populated.
                    if 'SecretString' in resp:
                        print("Found Secret String")
                        return resp['SecretString']
                    else:
                        print("Found Binary Secret")
                        return base64.b64decode(resp['SecretBinary'])
                except ClientError as err:
                    print('Error Talking to SecretsManager: ' + err.response['Error']['Code'] + ', Message: ' + str(err))
                    return None
      Description: A function to lookup and return user data from AWS Secrets Manager.
      Handler: index.lambda_handler
      Role:
        Fn::GetAtt: AuthenticationLambdaExecutionRole.Arn
      Runtime: python3.9
      Environment:
        Variables:
          SecretsManagerRegion:
            Fn::If:
              - SecretsManagerRegionProvided
              - Ref: SecretsManagerRegion
              - Ref: AWS::Region
  GetUserConfigLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:invokeFunction
      FunctionName:
        Fn::GetAtt: GetUserConfigLambda.Arn
      Principal: transfer.amazonaws.com
      SourceArn: !Sub "arn:${AWS::Partition}:transfer:${AWS::Region}:${AWS::AccountId}:server/*"
# Create Lambda function for Transfer Family Managed Workflow Alternate Workflow  
  ExceptionLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Join [ '', ['pgp-blog-exception', !Select [0, !Split [-, !Select [2, !Split [/, !Ref AWS::StackId ]]]]]]
      Timeout: 30
      MemorySize: 128
      Description: A function to handle exceptions in the workflow
      Handler: index.lambda_handler
      Role:
        Fn::GetAtt: ExceptionLambdaExecutionRole.Arn
      Runtime: python3.9
      Environment:
        Variables:
          SnsTopic: !Ref PGPblogSNStopic
      Code:
        ZipFile: |
          import boto3
          import os
          import datetime
          from datetime import timezone
          from botocore.exceptions import ClientError

          def lambda_handler(event, context):
              # Get info from the event
              token = event['token']
              workflow_id = event['serviceMetadata']['executionDetails']['workflowId']
              execution_id = event['serviceMetadata']['executionDetails']['executionId']
              serverId = event['serviceMetadata']['transferDetails']['serverId']
              username = event['serviceMetadata']['transferDetails']['userName']
              bucket_name = event['fileLocation']['bucket']
              source_key = event['fileLocation']['key']
              obj_name = source_key.split('/')[-1]
              
              # Build object key
              dest_key = 'quarantined-files/' + username + '/' + get_date_prefix() + '/' + obj_name
              # Copy file in quarantine prefix
              status = copy_file(bucket_name, source_key, dest_key)
              if status == 'FAILURE':
                  send_workflow_step(workflow_id, execution_id, token, status)
                  return{}
              # Delete original file
              status = delete_file(bucket_name, source_key)
              if status == 'FAILURE':
                  send_workflow_step(workflow_id, execution_id, token, status)
                  return{}
              # Send SNS notification
              status = send_notification(serverId, username, bucket_name, dest_key)
              send_workflow_step(workflow_id, execution_id, token, status)
              return {}
                      
          def get_date_prefix():
              today = datetime.datetime.now(timezone.utc)
              today = str(today)
              date_prefix = today[0:10]
              return str(date_prefix)
              
          def copy_file(bucket_name, source_key, dest_key):
              s3 = boto3.resource('s3')
              object_source = {
                  'Bucket': bucket_name,
                  'Key': source_key
              }
              bucket = s3.Bucket(bucket_name)
              dest_object = bucket.Object(dest_key)
              print('Trying to copy %s to %s' % (source_key, dest_key))
              try:
                  dest_object.copy(object_source)
                  print('Copy successful')
                  status = 'SUCCESS'
              except ClientError as err:
                  print('Error copying the source object: ' + err.response['Error']['Code'] + ', Message: ' + str(err))
                  status = 'FAILURE'
              return status
                  
          def delete_file(bucket_name, source_key):
              s3 = boto3.resource('s3')
              object = s3.Object(bucket_name,source_key)
              print('Deleting %s' % source_key)
              try:
                  object.delete()
                  status = 'SUCCESS'
              except ClientError as err:
                  print('Error deleting the source object: ' + err.response['Error']['Code'] + ', Message: ' + str(err))
                  status = 'FAILURE'
              return status
                  
          def send_notification(server_id, username, bucket_name, key):
              # s3uri = 's3://' + bucket_name + '/' + key
              obj_name = key.split('/')[-1]
              cli_command = 'aws s3api get-object --bucket ' + bucket_name + ' --key ' + key + ' ' + obj_name
              sns = boto3.client('sns')
              topic_arn = os.environ['SnsTopic']
              message = 'A file uploaded by user %s could not be decrypted. The file has been quarantined.\n\nTo download the file run the following CLI command:\n%s' % (username, cli_command)
              subject = 'PGP decryption error in server %s' % server_id
              try:
                  response = sns.publish(TopicArn=topic_arn,Message=message,Subject=subject)
                  print ('SNS response code: ' + str(response['ResponseMetadata']['HTTPStatusCode']))
                  status = 'SUCCESS'
              except ClientError as err:
                  print('Error sending message to SNS topic: ' + err.response['Error']['Code'] + ', Message: ' + str(err))
                  status = 'FAILURE'
              return status
                  
          def send_workflow_step(workflow_id, execution_id, token, status):
              transfer = boto3.client('transfer')
              response = transfer.send_workflow_step_state(
                  WorkflowId=workflow_id,
                  ExecutionId=execution_id,
                  Token=token,
                  Status=status
              )
