AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31

Description:
  (v2.1.0) Integration service between AWS MediaServices and Cloud DRM KMS Service
  Copyright 2019-2024, Insys Video Technologies sp. z o.o. sp. k. All Rights Reserved.

Parameters:
  AuthenticationMethod:
    Type: String
    Description: Authentication method
    Default: OAUTH
    AllowedValues:
      - OAUTH
      - X-API-KEY
  ClientId: 
    Type: String
    Description: OAUTH Client Id
  ClientSecret:
    Type: String
    NoEcho: true
    Description: OAUTH Client Secret
  TokenUrl:
    Type: String
    Description: OAUTH Token url
  ApiKey:
    Type: String
    NoEcho: true
    Description: X-API-KEY alternative authentication method
  SpekeUrl:
    Type: String
    Default: https://tenant-name.api.drm.cloud/kms/speke?tenantId=12345678-abcd-xxxx-xxxx-xxxxxxxxxxxx
    Description: Url to specify the SPEKE Live and VOD endpoint in Insys DRM Server that will provide keys for live or video on demand content
  GatewayType:
    Type: String
    Default: REGIONAL
    Description: Choose a type of gateway. Private is available only in VPC, Regional available only in deployed region, Edge Optimized is deployed over the CloudFront network.
    AllowedValues: 
      - EDGE
      - REGIONAL
      - PRIVATE
  EnableGatewayLogging:
    Type: String
    Default: false
    AllowedValues: [ true, false ]    
  LambdaLoggingLevel:
    Type: String
    Default: INFO
    AllowedValues:
      - DISABLED
      - INFO
      - DEBUG

Rules:
  CheckApiKeyWhenAuthMethodIsXApiKey:
    RuleCondition: !Equals
      - !Ref AuthenticationMethod
      - X-API-KEY
    Assertions:
      - Assert: !Not
        - !Equals
          - !Ref ApiKey
          - ''
        AssertDescription: 'ApiKey must not be empty for X-API-KEY AuthenticationMethod'
  CheckApiKeyWhenAuthMethodIsOAuth:
    RuleCondition: !Equals
      - !Ref AuthenticationMethod
      - OAUTH
    Assertions:
      - Assert: !Not
        - !Equals
          - !Ref ClientId
          - ''
        AssertDescription: 'ClientId must not be empty for OAUTH AuthenticationMethod'
      - Assert: !Not
        - !Equals
          - !Ref ClientSecret
          - ''
        AssertDescription: 'ClientSecret must not be empty for OAUTH AuthenticationMethod'
      - Assert: !Not 
        - !Equals 
          - !Ref TokenUrl
          - ''
        AssertDescription: 'TokenUrl must not be empty for OAUTH AuthenticationMethod'

Conditions:
  EnabledGatewayLogging: !Equals [true, !Ref EnableGatewayLogging]

Resources:
  ## == Authorization lambda resource == 
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${AWS::StackName}-LambdaRole"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: !Sub "${AWS::StackName}-LambdaPolicy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Sid: Logging
                Effect: Allow
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource: "*"

  AuthorizationLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub ${AWS::StackName}-Authorization
      Description: Authorization lambda for Live and VOD endpoint
      Role: !GetAtt LambdaRole.Arn
      Handler: index.lambda_handler
      Runtime: python3.12
      Code:
        ZipFile: |
          import os
          import json
          import urllib.parse
          import urllib.request

          class AccessToken:
            def __init__(self, access_token, token_type, expires_in):
              self.access_token = access_token
              self.token_type = token_type
              self.expires_in = expires_in

          def get_tenant_id(speke_url):
            url = urllib.parse.urlparse(speke_url)
            query_params = urllib.parse.parse_qs(url.query)
            return query_params.get('tenantId', [None])[0]

          def get_token_cloud_oauth(speke_url, token_url, client_id, client_secret, logging_level):
            if(logging_level in ["INFO", "DEBUG"]):
              print("Getting token in oauth flow for", speke_url)

            request_data = {
              'grant_type': 'client_credentials',
              'client_id': client_id,
              'client_secret': client_secret
            }

            headers = {
              'Accept': 'application/json',
              'Content-Type': 'application/x-www-form-urlencoded'
            }

            encoded_data = urllib.parse.urlencode(request_data).encode('utf-8')

            req = urllib.request.Request(token_url, headers=headers, data=encoded_data, method='POST')
            try:
              with urllib.request.urlopen(req) as response:
                response_data = json.loads(response.read().decode('utf-8'))
                access_token = AccessToken(
                  response_data["access_token"],
                  response_data["token_type"],
                  response_data["expires_in"]
                )
                return access_token
            except urllib.error.HTTPError as e:
              print(f"Error getting OAuth token: HTTP error: {e.code}, {e.reason}")
              return None
            except urllib.error.URLError as e:
              print(f"Error getting OAuth token: URL error: {e}")
              return None
            finally:
              if(logging_level in ["INFO", "DEBUG"]):
                print("End getting token for", speke_url)

          def forward_original_request(input_data, url, token=None, api_key=None, logging_level=False):
            if(logging_level in ["INFO", "DEBUG"]):
              print("Start ForwardOriginalRequest")
              if(logging_level in ["DEBUG"]):
                print("ForwardOriginal INPUT", url)
                print("ForwardOriginal INPUT method=", input_data["httpMethod"])
                for header, value in input_data["headers"].items():
                  print(f"ForwardOriginal INPUT header = {header} | {value}")

            headers = {
              **({'Authorization': f"Bearer {token.access_token}"} if token != None else {}),
              **({'X-Api-Key': api_key} if api_key != None else {}),
              "X-Tenant-Id": get_tenant_id(url)
            }
            if(logging_level in ["DEBUG"]):
              for header, value in headers.items():
                print(f"ForwardOriginal ADDED header = {header} | {value}")

            if input_data.get("headers"):
              for header, value in input_data["headers"].items():
                if header.lower() == "host":                    
                  headers["Host"] = urllib.parse.urlparse(url).netloc
                  if(logging_level in ["DEBUG"]):
                    print(f"ForwardOriginal CHANGED header = Host | {urllib.parse.urlparse(url).netloc}")
                elif header.lower() == "content-type" and value == "application/json":
                  headers["content-type"] = "application/xml"
                  if(logging_level in ["DEBUG"]):
                    print(f"ForwardOriginal CHANGED header = content-type | application/xml")
                else:
                  headers[header] = value

            try:
              body = input_data["body"]
              if(logging_level in ["DEBUG"]):
                print(f"ForwardOriginal INPUT body = {body}")

              req = urllib.request.Request(url, headers=headers, data=body.encode('utf-8'), method=input_data["httpMethod"])
              with urllib.request.urlopen(req) as response:
                response_body = response.read().decode('utf-8')
                response_headers = {header: value for header, value in response.headers.items()}
                if(logging_level in ["DEBUG"]):
                  print("ForwardOriginalRequest url=", url)
                  print("ForwardOriginalRequest response body=", response_body)                
                  for header, value in response_headers.items():
                    print(f"ForwardOriginalRequest response headers = {header} | {value}")
                if(logging_level in ["INFO", "DEBUG"]):
                  print("End ForwardOriginalRequest")
                return {
                  "statusCode": response.status,
                  "body": response_body,
                  "headers": response_headers
                }              
            except urllib.error.HTTPError as e:
              print(f"Error invoking token: HTTP error: {e.code}, {e.reason}")
              return {
                "statusCode": 500
              }
            except urllib.error.URLError as e:
              print(f"Error invoking token: URL error: {e}")    
              return {
                "statusCode": 500
              }

          def lambda_handler(event, context):
            logging_level = os.getenv("logging_level")
            if(logging_level in ["INFO", "DEBUG"]):
              print("Started authorization lambda")

            authentication_method = os.getenv("authentication_method")    
            client_id = os.getenv("client_id")
            client_secret = os.getenv("client_secret")
            api_key = os.getenv("api_key")
            speke_url = os.getenv("speke_url")
            token_url = os.getenv("token_url")

            if not speke_url:
              return {
                "statusCode": 400,
                "body": "Variable 'speke_url' missing",
                "headers": {"Content-Type": "text/plain"}
              }
            
            if authentication_method == "OAUTH":
              if not client_id:
                raise Exception("Variable 'client_id' missing")
              if not client_secret:
                raise Exception("Variable 'client_secret' missing")

              token = get_token_cloud_oauth(speke_url, token_url, client_id, client_secret, logging_level)
              if token:
                response = forward_original_request(event, url=speke_url, token=token, logging_level=logging_level)
                if(logging_level in ["INFO", "DEBUG"]):
                  print("Finished authorization lambda")
                return {
                  "statusCode": response["statusCode"], 
                  "body": response["body"], 
                  "headers": response["headers"]
                }
              else:
                if(logging_level in ["INFO", "DEBUG"]):
                  print("Finished authorization lambda")
                return {
                  "statusCode": 500,
                  "body": "Error getting OAuth token"
                }
            elif authentication_method == "X-API-KEY":
              if not api_key:
                raise Exception("Variable 'api_key' missing")
              
              response = forward_original_request(event, url=speke_url, api_key=api_key, logging_level=logging_level)

              if(logging_level in ["INFO", "DEBUG"]):
                print("Finished authorization lambda")

              return {
                "statusCode": response["statusCode"], 
                "body": response["body"], 
                "headers": response["headers"]
              }
            else: 
              raise Exception("Unknown authentication method")
      Environment:
        Variables:
          authentication_method: !Ref AuthenticationMethod
          client_id: !Ref ClientId
          client_secret: !Ref ClientSecret
          api_key: !Ref ApiKey
          speke_url: !Ref SpekeUrl
          token_url: !Ref TokenUrl
          logging_level: !Ref LambdaLoggingLevel
      Timeout: 30
      MemorySize: 512

  InvokeAuthorizationLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt AuthorizationLambda.Arn
      Action: lambda:InvokeFunction
      Principal: apigateway.amazonaws.com

  AuthorizationLambdaLog:
    Type: AWS::Logs::LogGroup
    DependsOn: 
      - AuthorizationLambda
    Properties:
      LogGroupName: !Join ["/", ["/aws/lambda", !Ref AuthorizationLambda]]
      RetentionInDays: 90

  ## == Api Gateway ==
  ApiGateway:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: !Sub ${AWS::StackName}-GatewayApi
      Parameters:
        endpointConfigurationTypes: !Ref GatewayType

  # VOD method
  ApiGatewayVodResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref ApiGateway
      ParentId: !GetAtt ApiGateway.RootResourceId
      PathPart: "vod"

  ApiGatewayVodMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      HttpMethod: POST
      AuthorizationType: AWS_IAM
      RestApiId: !Ref ApiGateway
      ResourceId: !Ref ApiGatewayVodResource
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub
          - arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Arn}/invocations
          - Arn: !GetAtt AuthorizationLambda.Arn
      MethodResponses:
        - StatusCode: 200
        - StatusCode: 500

  # Live method
  ApiGatewayLiveResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref ApiGateway
      ParentId: !GetAtt ApiGateway.RootResourceId
      PathPart: "live"

  ApiGatewayLiveMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      HttpMethod: POST
      AuthorizationType: AWS_IAM
      RestApiId: !Ref ApiGateway
      ResourceId: !Ref ApiGatewayLiveResource
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub
          - arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Arn}/invocations
          - Arn: !GetAtt AuthorizationLambda.Arn
      MethodResponses:
        - StatusCode: 200
        - StatusCode: 500

  ApiGatewayDeploymentVer2:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
      - ApiGatewayVodMethod
      - ApiGatewayLiveMethod
    Properties:
      RestApiId: !Ref ApiGateway
      StageName: v1

  ApiGatewayStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      DeploymentId: !Ref ApiGatewayDeploymentVer2
      MethodSettings:
        - DataTraceEnabled: true
          HttpMethod: "*"
          LoggingLevel: !If [ EnabledGatewayLogging, INFO, !Ref "AWS::NoValue" ] 
          ResourcePath: "/*"
      RestApiId: !Ref ApiGateway
      StageName: LATEST

  ApiGatewayLatestStageLog:
    Type: AWS::Logs::LogGroup
    Condition: EnabledGatewayLogging 
    DependsOn: 
      - AuthorizationLambda
    Properties:
      LogGroupName: !Sub "${AWS::StackName}/apigateway/${ApiGateway}/LATEST"
      RetentionInDays: 90

  ## == IAM Roles == 
  MediaPackageRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${AWS::StackName}-MediaPackageRole"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - mediapackage.amazonaws.com
                - mediapackagev2.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: !Sub "${AWS::StackName}-invokeLiveSpekeEndpointPolicy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Sid: invokeSpekePolicy
                Effect: Allow
                Action: "execute-api:Invoke"
                Resource: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/POST/live"

  MediaConvertRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${AWS::StackName}-MediaConvertRole"
      Description: "ARN of IAM Role for MediaConvert with access to SpekeGateway's VOD endpoint and full access to S3"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                -  mediaconvert.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonS3FullAccess        
      Policies:
        - PolicyName: !Sub "invokeVodSpekeEndpointPolicy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Sid: invokeSpekePolicy
                Effect: Allow
                Action: "execute-api:Invoke"
                Resource: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/POST/vod"

  MediaPackageVodRole:
    Type: AWS::IAM::Role
    Properties:
      Description: "ARN of IAM Role for MediaPackage VOD with access to SpekeGateway's VOD endpoint"
      RoleName: !Sub "${AWS::StackName}-MediaPackageVodRole"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                -  mediapackage.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: !Sub "invokeVodSpekeEndpointPolicy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Sid: invokeSpekePolicy
                Effect: Allow
                Action: "execute-api:Invoke"
                Resource: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/POST/vod"

Outputs:
  LiveSpekeInvokeUrl: 
    Description: "SpekeGateway endpoint uses for Live content"
    Value: !Sub "https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/LATEST/live"
    Export:
      Name: !Sub "${AWS::StackName}:LiveSpekeInvokeUrl"
  VodSpekeInvokeUrl:
    Description: "SpekeGateway endpoint uses for VOD content"
    Value: !Sub "https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/LATEST/vod"
    Export:
      Name: !Sub "${AWS::StackName}:VodSpekeInvokeUrl"
  MediaPackageRole:
    Description: "ARN of IAM Role for MediaPackage Live with access to SpekeGateway's Live endpoint"
    Value: !GetAtt MediaPackageRole.Arn
    Export:
      Name: !Sub "${AWS::StackName}:MediaPackageRole"
  MediaConvertRole:
    Description: "ARN of IAM Role for MediaConvert with access to SpekeGateway VOD endpoint and full access to S3"
    Value: !GetAtt MediaConvertRole.Arn
    Export:
      Name: !Sub "${AWS::StackName}:MediaConvertRole"
  MediaPackageVodRole:
    Description: "ARN of IAM Role for MediaPackage VOD with access to SpekeGateway's VOD endpoint"
    Value: !GetAtt MediaPackageVodRole.Arn
    Export:
      Name: !Sub "${AWS::StackName}:MediaPackageVodRole"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: 'Authentication config'
        Parameters:
          - AuthenticationMethod 
          - ClientId
          - ClientSecret
          - TokenUrl
          - ApiKey
      - Label:
          default: 'Cloud Video Kit configuration'
        Parameters:
          - SpekeUrl          
      - Label:
          default: 'Technical parameters'
        Parameters:
          - GatewayType
          - EnableGatewayLogging
          - LambdaLoggingLevel