---
AWSTemplateFormatVersion: 2010-09-09
Description: >-
  This template creates an AWS Transfer Family server with the AS2 protocol
  enabled. It configures the server with a public VPC endpoint, certificates,
  local and partner profiles, an agreement, and a connector.
  **WARNING** You will be billed for the AWS resources used if you create a
  stack from this template.

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: AS2
        Parameters:
          - LocalAs2Id
          - PartnerAs2Id
      - Label:
          default: Network
        Parameters:
          - SecurityGroupIngressCidrIp
      - Label:
          default: General
        Parameters:
          - Prefix
    ParameterLabels:
      LocalAs2Id:
        default: Local AS2 ID
      PartnerAs2Id:
        default: Partner AS2 ID
      SecurityGroupIngressCidrIp:
        default: Security group ingress CIDR IP

Parameters:
  Prefix:
    Type: String
    Description: Unique name prefix for resources that are created by the stack.
    ConstraintDescription: >-
      must not start with a dash, and must only contain lowercase a-z, digits,
      and a dash.
    AllowedPattern: ^[a-z0-9][a-z0-9-]+$
    MinLength: 1
    MaxLength: 30
    Default: transfer-as2

  LocalAs2Id:
    Type: String
    Description: AS2 ID of the local party.
    ConstraintDescription: >-
      must follow the AS2 system identifiers format described in RFC 4130 § 6.2.
    AllowedPattern: ^[\p{Print}\s]*$
    MinLength: 1
    MaxLength: 128
    Default: local

  PartnerAs2Id:
    Type: String
    Description: AS2 ID of the partner.
    ConstraintDescription: >-
      must follow the AS2 system identifiers format described in RFC 4130 § 6.2.
    AllowedPattern: ^[\p{Print}\s]*$
    MinLength: 1
    MaxLength: 128
    Default: partner

  SecurityGroupIngressCidrIp:
    Type: String
    Description: >-
      IPv4 address in CIDR format for allowed incoming traffic to the AS2
      server. Defaults to allowing all IPs.
    ConstraintDescription: >-
      must be in the form x.x.x.x/s, where x is 0-255, and s is 0-32.
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    Default: 0.0.0.0/0

Resources:
  TransferCloudWatchLoggingRole:
    Type: AWS::IAM::Role
    Properties:
      Description: >-
        IAM role used by Transfer Family AS2 to log requests to CloudWatch.
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - transfer.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSTransferLoggingAccess

  TransferSecretCustomResourceLambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      Description: >-
        IAM role used by a CloudWatch custom resource to execute a Lambda which
        manages a secret in Secrets Manager for Transfer Family AS2.
      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:
            Fn::Sub: ${Prefix}-TransferSecret.ARNsManagerWritePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - secretsmanager:CreateSecret
                  - secretsmanager:DeleteSecret
                Resource:
                  - Fn::Sub: arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:aws/transfer/${Prefix}*

  TransferS3AccessRole:
    Type: AWS::IAM::Role
    Properties:
      Description: >-
        IAM role used by a Transfer Family AS2 agreement and connector to
        transfer files in and out of an S3 bucket.
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - transfer.amazonaws.com
            Action:
              - sts:AssumeRole

  TransferS3AccessPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName:
        Fn::Sub: ${Prefix}-TransferS3AccessPolicy
      Roles:
        - Ref: TransferS3AccessRole
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - s3:ListBucket
            Resource:
              - Fn::GetAtt: TransferS3Bucket.Arn
          - Effect: Allow
            Action:
              - s3:PutObject
              - s3:GetObject
              - s3:GetObjectVersion
              - s3:DeleteObject
              - s3:DeleteObjectVersion
              - s3:GetBucketLocation
            Resource:
              - Fn::GetAtt: TransferS3Bucket.Arn
              - Fn::Sub:
                  - ${BucketARN}/*
                  - BucketARN:
                      Fn::GetAtt: TransferS3Bucket.Arn

  TransferS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      AccessControl: Private
      BucketName:
        Fn::Sub: ${Prefix}-${AWS::Region}-${AWS::AccountId}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      VersioningConfiguration:
        Status: Enabled

  TransferVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16

  TransferSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.1.0/24
      VpcId:
        Ref: TransferVPC

  TransferInternetGateway:
    Type: AWS::EC2::InternetGateway

  TransferVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId:
        Ref: TransferInternetGateway
      VpcId:
        Ref: TransferVPC

  TransferRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: TransferVPC

  TransferRoute:
    Type: AWS::EC2::Route
    DependsOn:
      - TransferVPCGatewayAttachment
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: TransferInternetGateway
      RouteTableId:
        Ref: TransferRouteTable

  TransferSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: TransferRouteTable
      SubnetId:
        Ref: TransferSubnet

  TransferElasticIp:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  TransferSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: >-
        Allows incoming traffic on port 5080 for AS2 messages on a Transfer
        Family server and denies all outgoing traffic.
      SecurityGroupEgress:
        - Description: Denies all outgoing traffic.
          IpProtocol: -1
          CidrIp: 0.0.0.0/32
      SecurityGroupIngress:
        - Description: Allows incoming TCP traffic on port 5080.
          IpProtocol: tcp
          FromPort: 5080
          ToPort: 5080
          CidrIp:
            Ref: SecurityGroupIngressCidrIp
      VpcId:
        Ref: TransferVPC

  TransferServer:
    Type: AWS::Transfer::Server
    Properties:
      Domain: S3
      EndpointDetails:
        AddressAllocationIds:
          - Fn::GetAtt: TransferElasticIp.AllocationId
        SecurityGroupIds:
          - Ref: TransferSecurityGroup
        SubnetIds:
          - Ref: TransferSubnet
        VpcId:
          Ref: TransferVPC
      EndpointType: VPC
      LoggingRole:
        Fn::GetAtt: TransferCloudWatchLoggingRole.Arn
      ProtocolDetails:
        As2Transports:
          - HTTP
      Protocols:
        - AS2

  TransferSecretCustomResourceLambda:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import datetime
          import json
          import site
          import subprocess
          from pathlib import Path

          import boto3
          import cfnresponse

          DAY = datetime.timedelta(1, 0, 0)


          def provision_packages():
              # Use a site dir in /tmp since that's the only read-write directory.
              site_dir = Path("/tmp/site-packages")
              site_dir.mkdir(parents=True, exist_ok=True)
              site.addsitedir(str(site_dir))

              # For demo purposes only! In production, use a Lambda layer to provision packages.
              subprocess.run(
                  ["pip", "install", "--target", str(site_dir), "cryptography"],
                  capture_output=True,
                  check=True,
                  text=True,
              )


          def generate_cert(common_name):
              from cryptography import x509
              from cryptography.hazmat.primitives import hashes, serialization
              from cryptography.hazmat.primitives.asymmetric import rsa
              from cryptography.x509.oid import NameOID

              private_key = rsa.generate_private_key(
                  public_exponent=65537,
                  key_size=2048,
              )

              name = x509.Name(
                  [
                      x509.NameAttribute(NameOID.COMMON_NAME, common_name),
                      x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
                      x509.NameAttribute(NameOID.LOCALITY_NAME, "Boston"),
                      x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Company"),
                      x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Unit"),
                      x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "MA"),
                  ]
              )

              builder = x509.CertificateBuilder()
              builder = builder.issuer_name(name)
              builder = builder.not_valid_after(datetime.datetime.today() + (DAY * 365))
              builder = builder.not_valid_before(datetime.datetime.today() - DAY)
              builder = builder.public_key(private_key.public_key())
              builder = builder.serial_number(x509.random_serial_number())
              builder = builder.subject_name(name)

              certificate = builder.sign(
                  private_key=private_key,
                  algorithm=hashes.SHA256(),
              )

              return (
                  certificate.public_bytes(serialization.Encoding.PEM).decode("ascii"),
                  private_key.private_bytes(
                      serialization.Encoding.PEM,
                      serialization.PrivateFormat.PKCS8,
                      serialization.NoEncryption(),
                  ).decode("ascii"),
              )


          def format_secret_name(event):
              prefix = event["ResourceProperties"]["Prefix"]
              return f"aws/transfer/{prefix}"


          def handle_create(event, context, secretsmanager):
              provision_packages()

              local_cert, local_key = generate_cert("local")
              partner_cert, partner_key = generate_cert("partner")
              secret = dict(
                  LocalCertificate=local_cert,
                  LocalPrivateKey=local_key,
                  PartnerCertificate=partner_cert,
                  PartnerPrivateKey=partner_key,
              )

              response = secretsmanager.create_secret(
                  Name=format_secret_name(event),
                  Description=f"Self-signed X.509 certificates and RSA private keys for AS2.",
                  SecretString=json.dumps(secret),
              )
              cfnresponse.send(event, context, cfnresponse.SUCCESS, response, reason="Created")


          def handle_delete(event, context, secretsmanager):
              secretsmanager.delete_secret(
                  SecretId=format_secret_name(event),
                  ForceDeleteWithoutRecovery=True,
              )
              cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, reason="Deleted")


          def lambda_handler(event, context):
              try:
                  secretsmanager = boto3.client("secretsmanager")

                  if event["RequestType"] == "Create":
                      handle_create(event, context, secretsmanager)
                  elif event["RequestType"] == "Delete":
                      handle_delete(event, context, secretsmanager)
              except Exception:
                  cfnresponse.send(event, context, cfnresponse.FAILED, {})
                  raise
      Description: >-
        Create a secret in Secrets Manager containing two pairs of self-signed
        X.509 certificates and RSA private keys for AS2.
      Handler: index.lambda_handler
      Role:
        Fn::GetAtt: TransferSecretCustomResourceLambdaExecutionRole.Arn
      Runtime: python3.11
      Timeout: 60

  TransferSecret:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken:
        Fn::GetAtt: TransferSecretCustomResourceLambda.Arn
      Prefix:
        Ref: Prefix

  TransferLocalEncryptionCertificatePrivate:
    Type: AWS::Transfer::Certificate
    Properties:
      Certificate:
        Fn::Sub: "{{resolve:secretsmanager:${TransferSecret.ARN}:SecretString:LocalCertificate}}"
      Description:
        Fn::Sub: ${Prefix}-local-encryption-private-key
      PrivateKey:
        Fn::Sub: "{{resolve:secretsmanager:${TransferSecret.ARN}:SecretString:LocalPrivateKey}}"
      Usage: ENCRYPTION

  TransferLocalSigningCertificatePrivate:
    Type: AWS::Transfer::Certificate
    Properties:
      Certificate:
        Fn::Sub: "{{resolve:secretsmanager:${TransferSecret.ARN}:SecretString:LocalCertificate}}"
      Description:
        Fn::Sub: ${Prefix}-local-signing-private-key
      PrivateKey:
        Fn::Sub: "{{resolve:secretsmanager:${TransferSecret.ARN}:SecretString:LocalPrivateKey}}"
      Usage: SIGNING

  TransferPartnerEncryptionCertificatePrivate:
    Type: AWS::Transfer::Certificate
    Properties:
      Certificate:
        Fn::Sub: "{{resolve:secretsmanager:${TransferSecret.ARN}:SecretString:PartnerCertificate}}"
      Description:
        Fn::Sub: ${Prefix}-partner-encryption-private-key
      PrivateKey:
        Fn::Sub: "{{resolve:secretsmanager:${TransferSecret.ARN}:SecretString:PartnerPrivateKey}}"
      Usage: ENCRYPTION

  TransferPartnerSigningCertificatePrivate:
    Type: AWS::Transfer::Certificate
    Properties:
      Certificate:
        Fn::Sub: "{{resolve:secretsmanager:${TransferSecret.ARN}:SecretString:PartnerCertificate}}"
      Description:
        Fn::Sub: ${Prefix}-partner-signing-private-key
      PrivateKey:
        Fn::Sub: "{{resolve:secretsmanager:${TransferSecret.ARN}:SecretString:PartnerPrivateKey}}"
      Usage: SIGNING

  TransferLocalEncryptionCertificate:
    Type: AWS::Transfer::Certificate
    Properties:
      Certificate:
        Fn::Sub: "{{resolve:secretsmanager:${TransferSecret.ARN}:SecretString:LocalCertificate}}"
      Description:
        Fn::Sub: ${Prefix}-local-encryption
      Usage: ENCRYPTION

  TransferLocalSigningCertificate:
    Type: AWS::Transfer::Certificate
    Properties:
      Certificate:
        Fn::Sub: "{{resolve:secretsmanager:${TransferSecret.ARN}:SecretString:LocalCertificate}}"
      Description:
        Fn::Sub: ${Prefix}-local-signing
      Usage: SIGNING

  TransferPartnerEncryptionCertificate:
    Type: AWS::Transfer::Certificate
    Properties:
      Certificate:
        Fn::Sub: "{{resolve:secretsmanager:${TransferSecret.ARN}:SecretString:PartnerCertificate}}"
      Description:
        Fn::Sub: ${Prefix}-partner-encryption
      Usage: ENCRYPTION

  TransferPartnerSigningCertificate:
    Type: AWS::Transfer::Certificate
    Properties:
      Certificate:
        Fn::Sub: "{{resolve:secretsmanager:${TransferSecret.ARN}:SecretString:PartnerCertificate}}"
      Description:
        Fn::Sub: ${Prefix}-partner-signing
      Usage: SIGNING

  TransferLocalProfileWithLocalId:
    Type: AWS::Transfer::Profile
    Properties:
      As2Id:
        Ref: LocalAs2Id
      CertificateIds:
        - Ref: TransferLocalEncryptionCertificatePrivate
        - Ref: TransferLocalSigningCertificatePrivate
      ProfileType: LOCAL

  TransferLocalProfileWithPartnerId:
    Type: AWS::Transfer::Profile
    Properties:
      As2Id:
        Ref: PartnerAs2Id
      CertificateIds:
        - Ref: TransferPartnerEncryptionCertificatePrivate
        - Ref: TransferPartnerSigningCertificatePrivate
      ProfileType: LOCAL

  TransferPartnerProfileWithLocalId:
    Type: AWS::Transfer::Profile
    Properties:
      As2Id:
        Ref: LocalAs2Id
      CertificateIds:
        - Ref: TransferLocalEncryptionCertificate
        - Ref: TransferLocalSigningCertificate
      ProfileType: PARTNER

  TransferPartnerProfileWithPartnerId:
    Type: AWS::Transfer::Profile
    Properties:
      As2Id:
        Ref: PartnerAs2Id
      CertificateIds:
        - Ref: TransferPartnerEncryptionCertificate
        - Ref: TransferPartnerSigningCertificate
      ProfileType: PARTNER

  TransferAgreement:
    Type: AWS::Transfer::Agreement
    Properties:
      AccessRole:
        Fn::GetAtt: TransferS3AccessRole.Arn
      BaseDirectory:
        Fn::Sub: /${TransferS3Bucket}/${TransferServer.ServerId}/from-${PartnerAs2Id}
      Description:
        Fn::Sub: ${LocalAs2Id}-to-${PartnerAs2Id}
      LocalProfileId:
        Fn::GetAtt: TransferLocalProfileWithLocalId.ProfileId
      PartnerProfileId:
        Fn::GetAtt: TransferPartnerProfileWithPartnerId.ProfileId
      ServerId:
        Fn::GetAtt: TransferServer.ServerId

  TransferConnector:
    Type: AWS::Transfer::Connector
    Properties:
      AccessRole:
        Fn::GetAtt: TransferS3AccessRole.Arn
      As2Config:
        Compression: DISABLED
        EncryptionAlgorithm: AES256_CBC
        LocalProfileId:
          Fn::GetAtt: TransferLocalProfileWithPartnerId.ProfileId
        MdnResponse: SYNC
        MdnSigningAlgorithm: DEFAULT
        MessageSubject: Demo
        PartnerProfileId:
          Fn::GetAtt: TransferPartnerProfileWithLocalId.ProfileId
        SigningAlgorithm: SHA256
      LoggingRole:
        Fn::GetAtt: TransferCloudWatchLoggingRole.Arn
      Url:
        Fn::Sub: http://${TransferServer.ServerId}.server.transfer.${AWS::Region}.${AWS::URLSuffix}:5080

Outputs:
  TransferS3BucketName:
    Description: >-
      Name of the S3 Bucket for the Transfer Family AS2 agreement and connector.
    Value:
      Ref: TransferS3Bucket
    Export:
      Name:
        Fn::Sub: ${Prefix}-TransferS3BucketName
  TransferServerId:
    Description: ID of the Transfer Family AS2 server.
    Value:
      Fn::GetAtt: TransferServer.ServerId
    Export:
      Name:
        Fn::Sub: ${Prefix}-TransferServer
  TransferPublicVPCEndpoint:
    Description: Public VPC endpoint for the Transfer Family AS2 server.
    Value:
      Fn::Sub: http://${TransferServer.ServerId}.server.transfer.${AWS::Region}.${AWS::URLSuffix}:5080
    Export:
      Name:
        Fn::Sub: ${Prefix}-TransferPublicVPCEndpoint
  TransferLocalAs2Id:
    Description: Local AS2 ID.
    Value:
      Ref: LocalAs2Id
    Export:
      Name:
        Fn::Sub: ${Prefix}-TransferLocalAs2Id
  TransferPartnerAs2Id:
    Description: Partner AS2 ID.
    Value:
      Ref: PartnerAs2Id
    Export:
      Name:
        Fn::Sub: ${Prefix}-TransferPartnerAs2Id
  TransferAgreementId:
    Description: >-
      ID of the Transfer Family agreement for the local party to receive AS2
      messages from the partner.
    Value:
      Fn::GetAtt: TransferAgreement.AgreementId
    Export:
      Name:
        Fn::Sub: ${Prefix}-TransferAgreementId
  TransferAgreementBaseDirectory:
    Description: Base S3 directory used by the Transfer Family AS2 agreement.
    Value:
        Fn::Sub: /${TransferS3Bucket}/${TransferServer.ServerId}/from-${PartnerAs2Id}
    Export:
      Name:
        Fn::Sub: ${Prefix}-TransferAgreementBaseDirectory
  TransferConnectorId:
    Description: >-
      ID of the Transfer Family connector for the partner to send AS2 messages
      to the local party.
    Value:
      Fn::GetAtt: TransferConnector.ConnectorId
    Export:
      Name:
        Fn::Sub: ${Prefix}-TransferConnectorId
  TransferExampleAs2Command:
    Description: >-
      Example command to send an AS2 message from the partner to the local
      party.
    Value:
      Fn::Sub: >-
        aws s3api put-object --bucket ${TransferS3Bucket} --key test.txt
        && aws transfer start-file-transfer --region ${AWS::Region}
        --connector-id ${TransferConnector.ConnectorId}
        --send-file-paths /${TransferS3Bucket}/test.txt
    Export:
      Name:
        Fn::Sub: ${Prefix}-TransferExampleAs2Command
  TransferSecretName:
    Description: >-
      Name of the secret in Secrets Manager which contains the generated
      certificates and private keys for AS2.
    Value:
      Fn::GetAtt: TransferSecret.Name
    Export:
      Name:
        Fn::Sub: ${Prefix}-TransferSecretname
