AWSTemplateFormatVersion: '2010-09-09'
Description: |
      Sets up a CloudFront distribution with a given API Gateway as a custom origin and a Cloudwatch Events Scheduled job to rotate API Keys.
      You will be billed for the AWS resources used if you create a stack from this template. **NOTICE** Copyright 2018 Amazon.com, Inc. or its affiliates.
      All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at
      http://www.apache.org/licenses/LICENSE-2.0 or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      See the License for the specific language governing permissions and limitations under the License.

Parameters:


  ApiURL:
    Type: String
    Description: Fill in this format <apiId>.execute-api.<region>.amazonaws.com/Prod

  ApiKeyRotationSchedule: 
    Description: Schedule to rotate API Keys e.g. Daily, Monthly, Bimonthly basis
    Type: String
    Default: Daily
    AllowedValues:
      - Daily
      - Fortnightly
      - Monthly
      - Bimonthly
      - Quarterly
    ConstraintDescription: Must be any of the available options

  Timestamp:
    Type: String
    Description: Fill in this format <Year>-<Month>-<Day>
    Default: 2018-04-02


Metadata: 
  AWS::CloudFormation::Interface: 
    ParameterGroups: 
      - 
        Label: 
          default: "API Gateway to send traffic to"
        Parameters: 
          - ApiURL
      - 
        Label: 
          default: "Rotating API Keys"
        Parameters: 
          - ApiKeyRotationSchedule
          - Timestamp    
    ParameterLabels:
      ApiURL:
        default: "API Gateway stage URL (no https://)"
      ApiKeyRotationSchedule: 
        default: "How often should I rotate?"
      Timestamp: 
        default: "Timestamp to be appended in API Key"


Mappings: 


  ScheduleMap: 
    CloudwatchEvents:  # You can find more granular intervals at Amazon CloudWatch Scheduled Events doc: https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html
      Daily: "rate(1 day)"
      Fortnightly: "rate(14 days)"
      Monthly: "rate(30 days)"
      Bimonthly: "rate(60 days)"
      Quarterly: "rate(90 days)"


Resources:

    S3BucketAccessLogs:
      Type: "AWS::S3::Bucket"
      DeletionPolicy: Retain # you will need to delete the bucket manually or else stack deletion will fail due to CloudFront logs


    CFDistribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          Logging:
            IncludeCookies: false
            Bucket: !Sub ${S3BucketAccessLogs}.s3.amazonaws.com
            Prefix: CloudFront-logs
          Enabled: true
          Comment: API Gateway Regional Endpoint Blog post
          Origins:
            -
              Id: APIGWRegional
              DomainName: !Select [0, !Split ['/', !Ref ApiURL]]
              CustomOriginConfig:
                HTTPPort: 443
                OriginProtocolPolicy: https-only
              OriginCustomHeaders:
                - 
                  HeaderName: x-api-key
                  HeaderValue: !GetAtt GetApiKeyValue.Value
          DefaultCacheBehavior:
            TargetOriginId: APIGWRegional
            ForwardedValues:
              QueryString: false
              Cookies:
                Forward: none
            ViewerProtocolPolicy: redirect-to-https
            AllowedMethods:
              - DELETE
              - GET
              - HEAD
              - OPTIONS
              - PATCH
              - POST
              - PUT

    ApiUsagePlan:
      Type: AWS::ApiGateway::UsagePlan
      Properties:
        Description: CloudFront usage only
        UsagePlanName: CloudFront_only
        ApiStages:
          - 
            ApiId: !Select [0, !Split ['.', !Ref ApiURL]]
            Stage: !Select [1, !Split ['/', !Ref ApiURL]]

    ApiKey: 
      Type: "AWS::ApiGateway::ApiKey"
      Properties: 
        Name: !Sub "${AWS::StackName}-CloudFront-${Timestamp}"
        Description: !Sub "CloudFormation API Key ${Timestamp}"
        Enabled: true
        GenerateDistinctId: false
  
    ApiKeyUsagePlan:
      Type: "AWS::ApiGateway::UsagePlanKey"
      Properties:
        KeyId: !Ref ApiKey
        KeyType: API_KEY
        UsagePlanId: !Ref ApiUsagePlan

# Custom Resource to retrieve API Key value instead of ID

    GetApiKeyValue:
        DependsOn: "ApiKey"
        Type: Custom::ApiKeyValue
        Properties:
          ServiceToken: !GetAtt GetApiKeyValueFunction.Arn
          Timestamp: !Ref Timestamp

    GetApiKeyValueFunctionRole:
        Type: "AWS::IAM::Role"
        Properties:
            Path: "/"
            ManagedPolicyArns:
                - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
            AssumeRolePolicyDocument:
              Version: "2012-10-17"
              Statement:
                -
                  Sid: "AllowLambdaServiceToAssumeRole"
                  Effect: "Allow"
                  Action: 
                    - "sts:AssumeRole"
                  Principal:
                    Service: 
                      - "lambda.amazonaws.com"
            Policies:
              - 
                PolicyName: RotateKeysPolicy
                PolicyDocument:
                  Version: '2012-10-17'
                  Statement:
                    - 
                      Effect: Allow
                      Action: 'apigateway:GET'
                      Resource: '*'

    GetApiKeyValueFunction:
      Type: "AWS::Lambda::Function"
      Properties:
        Handler: "index.lambda_handler"
        Role: !GetAtt GetApiKeyValueFunctionRole.Arn
        Runtime: python3.6
        Timeout: 120
        Environment:
          Variables:
            API_KEY: !Ref ApiKey
        Code:
          ZipFile: !Sub |
            import datetime
            import os
            
            import boto3
            import cfnresponse
            from botocore.exceptions import ClientError
  
            session = boto3.Session()
            apigw = session.client('apigateway')
            
            
            def lambda_handler(event, context):
              request_type = event['RequestType']
              request_type_allowed = ["Update", "Create"]
              from pprint import pprint
              responseData = {}

              print("Cloudformation Operation ==> ", request_type)

              if request_type in request_type_allowed:
                try:
                  api_key = apigw.get_api_key(apiKey=os.getenv("API_KEY"), includeValue=True)
                  responseData = {'Value': api_key['value']}
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
                except ClientError as err:
                  responseData = {"Error": "{}".format(err)}
                  cfnresponse.send(event, context, cfnresponse.FAILED, responseData)
              else:
                cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)

# API Key Rotation Lambda Function, Permissions and Schedule

    RotateApiKeysFunction:
      Type: "AWS::Lambda::Function"
      Properties:
        Handler: "index.lambda_handler"
        Role: !GetAtt RotateApiKeysFunctionRole.Arn
        Runtime: python3.6
        Environment:
          Variables:
            StackName: !Ref "AWS::StackName"
        Code:
          ZipFile: !Sub |
            import datetime
            import os

            import boto3
            from botocore.exceptions import ClientError

            session = boto3.Session()
            cfn = session.client('cloudformation')
            
            timestamp = datetime.date.today()            
            params = {
                'StackName': os.getenv('StackName'),
                'UsePreviousTemplate': True,
                'Capabilities': ["CAPABILITY_IAM"],
                'Parameters': [
                    {
                      'ParameterKey': 'ApiURL',
                      'UsePreviousValue': True
                    },
                    {
                      'ParameterKey': 'ApiKeyRotationSchedule',
                      'UsePreviousValue': True
                    },
                    {
                      'ParameterKey': 'Timestamp',
                      'ParameterValue': str(timestamp)
                    },
                ],                
            }

            def lambda_handler(event, context):
              """ Updates CloudFormation Stack with a new timestamp and returns CloudFormation response"""
              try:
                  response = cfn.update_stack(**params)
              except ClientError as err:
                  if "No updates are to be performed" in err.response['Error']['Message']:
                      return {"message": err.response['Error']['Message']}
                  else:
                      raise Exception("An error happened whilst updating the stack: {}".format(err))          

              return response


    RotateApiKeysFunctionRole:
        Type: "AWS::IAM::Role"
        Properties:
            Path: "/"
            ManagedPolicyArns:
                - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
            AssumeRolePolicyDocument:
              Version: "2012-10-17"
              Statement:
                -
                  Sid: "AllowLambdaServiceToAssumeRole"
                  Effect: "Allow"
                  Action: 
                    - "sts:AssumeRole"
                  Principal:
                    Service: 
                      - "lambda.amazonaws.com"
            Policies:
              - 
                PolicyName: RotateKeysPolicy
                PolicyDocument:
                  Version: '2012-10-17'
                  Statement:
                    - 
                      Effect: Allow
                      Action: 'cloudformation:UpdateStack'
                      Resource: !Sub "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}/*"



    RotateApiKeysScheduledJob: 
      Type: "AWS::Events::Rule"
      Properties: 
        Description: "ScheduledRule"
        ScheduleExpression: !FindInMap [ScheduleMap, CloudwatchEvents, !Ref ApiKeyRotationSchedule]
        State: "ENABLED"
        Targets: 
          - 
            Arn: !GetAtt RotateApiKeysFunction.Arn
            Id: "RotateApiKeys"


    RotateApiKeysScheduledJobPermissions: 
      Type: "AWS::Lambda::Permission"
      Properties: 
        FunctionName: !Ref RotateApiKeysFunction
        Action: "lambda:InvokeFunction"
        Principal: "events.amazonaws.com"
        SourceArn: !GetAtt RotateApiKeysScheduledJob.Arn


Outputs:
  
    CFDistribution: 
      Description: CloudFront Domain URL
      Value: !Sub 
        - "https://${CFDistribution.DomainName}/${ApiStage}"
        - { ApiStage: !Select [1, !Split ['/', !Ref ApiURL]] }

    S3BucketAccessLogs:
      Description: S3 Bucket for CloudFront Access Logs
      Value: !Ref S3BucketAccessLogs

    ApiUsagePlanId:
      Description: API Gateway Usage Plan
      Value: !Ref ApiUsagePlan

    RotateApiKeysFunction:
      Description: Lambda function that rotates API Key
      Value: !GetAtt RotateApiKeysFunction.Arn

    RotateApiKeysFunctionRole:
      Description: Lambda function IAM Role
      Value: !GetAtt RotateApiKeysFunctionRole.Arn

