---
AWSTemplateFormatVersion: '2010-09-09'
Description:
  A basic template for creating a Lambda-backed API Gateway for use as
  a custom identity provider in AWS Transfer for SFTP. It authenticates against an
  entry in AWS Secrets Manager of the format SFTP/username. Additionaly, the secret
  must hold the key-value pairs for all user properties returned to AWS Transfer.
Parameters:
  CreateServer:
    AllowedValues:
      - 'true'
      - 'false'
    Type: String
    Description:
      Whether this stack creates a server internally or not. If a server is created internally,
      the customer identity provider is automatically associated with it.
    Default: 'true'
  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 SecretsMangager is not available.
    Default: ''
Conditions:
  CreateServer:
    Fn::Equals:
      - Ref: CreateServer
      - 'true'
  NotCreateServer:
    Fn::Not:
      - Condition: CreateServer
  SecretsManagerRegionProvided:
    Fn::Not:
      - Fn::Equals:
          - Ref: SecretsManagerRegion
          - ''
Outputs:
  ServerId:
    Value:
      Fn::GetAtt: TransferServer.ServerId
    Condition: CreateServer
  StackArn:
    Value:
      Ref: AWS::StackId
  TransferIdentityProviderUrl:
    Description: URL to pass to AWS Transfer CreateServer call as part of optional IdentityProviderDetails
    Value:
      Fn::Join:
        - ''
        - - https://
          - Ref: CustomIdentityProviderApi
          - .execute-api.
          - Ref: AWS::Region
          - .amazonaws.com/
          - Ref: ApiStage
    Condition: NotCreateServer
  TransferIdentityProviderInvocationRole:
    Description: IAM Role to pass to AWS Transfer CreateServer call as part of optional IdentityProviderDetails
    Value:
      Fn::GetAtt: TransferIdentityProviderRole.Arn
    Condition: NotCreateServer
Resources:
  TransferServer:
    Type: AWS::Transfer::Server
    Condition: CreateServer
    Properties:
      EndpointType: PUBLIC
      IdentityProviderDetails:
        InvocationRole:
          Fn::GetAtt: TransferIdentityProviderRole.Arn
        Url:
          Fn::Join:
            - ''
            - - https://
              - Ref: CustomIdentityProviderApi
              - .execute-api.
              - Ref: AWS::Region
              - .amazonaws.com/
              - Ref: ApiStage
      IdentityProviderType: API_GATEWAY
      LoggingRole:
        Fn::GetAtt: CloudWatchLoggingRole.Arn
  CloudWatchLoggingRole:
    Description: IAM role used by Transfer  to log API requests to CloudWatch
    Type: AWS::IAM::Role
    Condition: CreateServer
    Properties:
      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: '*'
  CustomIdentityProviderApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: Transfer Identity Provider API
      Description: API used for GetUserConfig requests
      FailOnWarnings: true
      EndpointConfiguration:
        Types:
          - REGIONAL
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws: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:secretsmanager:${SecretsRegion}:${AWS::AccountId}:secret:SFTP/*
                    - SecretsRegion:
                        Fn::If:
                          - SecretsManagerRegionProvided
                          - Ref: SecretsManagerRegion
                          - Ref: AWS::Region
  ApiCloudWatchLogsRole:
    Description: IAM role used by API Gateway to log API requests to CloudWatch
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - apigateway.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: ApiGatewayLogsPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:DescribeLogGroups
                  - logs:DescribeLogStreams
                  - logs:PutLogEvents
                  - logs:GetLogEvents
                  - logs:FilterLogEvents
                Resource: '*'
  ApiLoggingAccount:
    Type: AWS::ApiGateway::Account
    DependsOn:
      - CustomIdentityProviderApi
    Properties:
      CloudWatchRoleArn:
        Fn::GetAtt: ApiCloudWatchLogsRole.Arn
  ApiStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      DeploymentId:
        Ref: ApiDeployment
      MethodSettings:
        - DataTraceEnabled: false
          HttpMethod: '*'
          LoggingLevel: INFO
          ResourcePath: '/*'
      RestApiId:
        Ref: CustomIdentityProviderApi
      StageName: prod
  ApiDeployment:
    DependsOn:
      - GetUserConfigRequest
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId:
        Ref: CustomIdentityProviderApi
      StageName: production
  TransferIdentityProviderRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: transfer.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: TransferCanInvokeThisApi
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - execute-api:Invoke
                Resource:
                  Fn::Sub: arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${CustomIdentityProviderApi}/prod/GET/*
        - PolicyName: TransferCanReadThisApi
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - apigateway:GET
                Resource: '*'
  GetUserConfigLambda:
    Type: AWS::Lambda::Function
    Properties:
      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']
                print("Username: {}, ServerId: {}".format(input_username, event['serverId']));

                if 'password' in event:
                    input_password = event['password']
                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("SFTP/" + 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']]
                    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: LambdaExecutionRole.Arn
      Runtime: python3.7
      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: apigateway.amazonaws.com
      SourceArn:
        Fn::Join:
          - ''
          - - 'arn:aws:execute-api:'
            - Ref: AWS::Region
            - ':'
            - Ref: AWS::AccountId
            - ':'
            - Ref: CustomIdentityProviderApi
            - '/*'
  ServersResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId:
        Ref: CustomIdentityProviderApi
      ParentId:
        Fn::GetAtt:
          - CustomIdentityProviderApi
          - RootResourceId
      PathPart: servers
  ServerIdResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId:
        Ref: CustomIdentityProviderApi
      ParentId:
        Ref: ServersResource
      PathPart: '{serverId}'
  UsersResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId:
        Ref: CustomIdentityProviderApi
      ParentId:
        Ref: ServerIdResource
      PathPart: users
  UserNameResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId:
        Ref: CustomIdentityProviderApi
      ParentId:
        Ref: UsersResource
      PathPart: '{username}'
  GetUserConfigResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId:
        Ref: CustomIdentityProviderApi
      ParentId:
        Ref: UserNameResource
      PathPart: config
  GetUserConfigRequest:
    Type: AWS::ApiGateway::Method
    DependsOn: GetUserConfigResponseModel
    Properties:
      AuthorizationType: AWS_IAM
      HttpMethod: GET
      Integration:
        Type: AWS
        IntegrationHttpMethod: POST
        Uri:
          Fn::Join:
            - ''
            - - 'arn:aws:apigateway:'
              - Ref: AWS::Region
              - ':lambda:path/2015-03-31/functions/'
              - Fn::GetAtt:
                  - GetUserConfigLambda
                  - Arn
              - '/invocations'
        IntegrationResponses:
          - StatusCode: 200
        RequestTemplates:
          application/json: |
            {
              "username": "$input.params('username')",
              "password": "$util.escapeJavaScript($input.params('Password')).replaceAll("\\'","'")",
              "protocol": "$input.params('protocol')",
              "serverId": "$input.params('serverId')",
              "sourceIp": "$input.params('sourceIp')"
            }
      RequestParameters:
        method.request.header.Password: false
        method.request.querystring.protocol: false
        method.request.querystring.sourceIp: false
      ResourceId:
        Ref: GetUserConfigResource
      RestApiId:
        Ref: CustomIdentityProviderApi
      MethodResponses:
        - StatusCode: 200
          ResponseModels:
            application/json: UserConfigResponseModel
  GetUserConfigResponseModel:
    Type: AWS::ApiGateway::Model
    Properties:
      RestApiId:
        Ref: CustomIdentityProviderApi
      ContentType: application/json
      Description: API response for GetUserConfig
      Name: UserConfigResponseModel
      Schema:
        '$schema': http://json-schema.org/draft-04/schema#
        title: UserUserConfig
        type: object
        properties:
          HomeDirectory:
            type: string
          Role:
            type: string
          Policy:
            type: string
          PublicKeys:
            type: array
            items:
              type: string
