---
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. 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.
  You can modify the Lambda function code to do something different after deployment.
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 SecretsManager 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
        - '.'
        - Ref: AWS::URLSuffix
        - '/'
        - 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
              - '.'
              - Ref: AWS::URLSuffix
              - '/'
              - 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 Custom Identity Provider basic template 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:
      - 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
  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:DescribeQueries
            - logs:FilterLogEvents
            - logs:GetLogEvents
            - logs:GetLogGroupFields
            - logs:GetLogRecord
            - logs:GetQueryResults
            - logs:PutLogEvents
            - logs:StartQuery
            - logs:StopQuery
            Resource: "*"
  ApiLoggingAccount:
    Type: AWS::ApiGateway::Account
    DependsOn:
    - CustomIdentityProviderApi
    Properties:
      CloudWatchRoleArn:
        Fn::GetAtt: ApiCloudWatchLogsRole.Arn
  ApiStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      DeploymentId:
        Ref: ApiDeployment202008
      MethodSettings:
      - DataTraceEnabled: false
        HttpMethod: "*"
        LoggingLevel: INFO
        ResourcePath: "/*"
      RestApiId:
        Ref: CustomIdentityProviderApi
      StageName: prod
  ApiDeployment202008:
    DependsOn:
    - GetUserConfigRequest
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId:
        Ref: CustomIdentityProviderApi
  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

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

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

            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):
                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.11
      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::Sub: arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${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:"
              - Ref: AWS::Partition
              - ":apigateway:"
              - Ref: AWS::Region
              - ":lambda:path/2015-03-31/functions/"
              - Fn::GetAtt:
                - GetUserConfigLambda
                - Arn
              - "/invocations"
        IntegrationResponses:
        - StatusCode: 200
        RequestTemplates:
          application/json: |
            {
              "username": "$util.urlDecode($input.params('username'))",
              "password": "$util.escapeJavaScript($util.base64Decode($input.params('PasswordBase64'))).replaceAll("\\'","'")",
              "protocol": "$input.params('protocol')",
              "serverId": "$input.params('serverId')",
              "sourceIp": "$input.params('sourceIp')"
            }
      RequestParameters:
        method.request.header.PasswordBase64: 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
