AWSTemplateFormatVersion: 2010-09-09

Parameters:
#-------Managed Active Directory Parameters-------#
  pS3BucketName:
    AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$
    Type: String
    Default: "awsiammedia"
    Description: S3 Bucket name for the AWS Lambda function code
  pS3KeySMRotate:
    Type: String
    Default: "public/sample/775-Store-rotate-Directory-Service-admin-password/code.zip"
    Description: S3 Bucket path
Resources:
  #--------Create the VPC & Subnets-------#
  BlogVPC:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: 172.31.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: BlogPost-VPC
  
  InternetGw:
    Type: 'AWS::EC2::InternetGateway'
    Properties:
      Tags:
        - Key: VPCName
          Value: BlogPost-VPC 

  NatGW:
     Type: 'AWS::EC2::NatGateway'
     Properties:
        AllocationId:
           Fn::GetAtt:
           - NatGwEIP
           - AllocationId
        SubnetId:
           Ref: PublicSubnet1
        Tags:
        - Key: Name
          Value: NatGW

  NatGwEIP:
     Type: 'AWS::EC2::EIP'
     Properties:
        Domain: vpc
    
  InternetGwAttach:
    Type: 'AWS::EC2::VPCGatewayAttachment'
    Properties:
      VpcId: !Ref BlogVPC
      InternetGatewayId: !Ref InternetGw
      
  PublicSubnet1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref BlogVPC
      AvailabilityZone:
        Fn::Select: 
          - 0
          - Fn::GetAZs: ""
      CidrBlock: 172.31.1.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: PublicSubnet1

  PrivateSubnet1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref BlogVPC
      AvailabilityZone:
        Fn::Select: 
          - 0
          - Fn::GetAZs: ""
      CidrBlock: 172.31.2.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: PrivateSubnet1

  PrivateSubnet2:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref BlogVPC
      AvailabilityZone:
        Fn::Select: 
          - 1
          - Fn::GetAZs: ""
      CidrBlock: 172.31.3.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: PrivateSubnet2
  
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref BlogVPC
      Tags:
        - Key: Name
          Value: "PublicRouteTable"
  
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref BlogVPC
      Tags:
        - Key: Name
          Value: "PrivateRouteTable"

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGwAttach
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGw

  PrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGW

  PublicSubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet1

  PrivateSubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet1

  PrivateSubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet2

  #-------Managed Active Directory-------#
  MSAD: 
    Type: AWS::DirectoryService::SimpleAD
    Properties: 
      Name: 'corp2.com'
      Password: !GetAtt RandomPassword.Data #This will be updated using the lambda function
      Size: Small
      VpcSettings: 
        SubnetIds: 
          - Ref: 'PrivateSubnet1'
          - Ref: 'PrivateSubnet2'
        VpcId: !Ref BlogVPC
  
  #-------DHCP Options Set -----------#
  DHCPSet: 
    Type: AWS::EC2::DHCPOptions
    Properties:
      DomainName: 'corp2.com'
      DomainNameServers: !GetAtt MSAD.DnsIpAddresses

  #-------DHCP VPC Association Set ----- #
  VPCDHCPAssoc: 
    Type: AWS::EC2::VPCDHCPOptionsAssociation
    Properties:
      DhcpOptionsId: !Ref DHCPSet
      VpcId: !Ref BlogVPC

  #-------DomainJoin Role ----------#
  SeamlessDomainJoinRole:
     Type: AWS::IAM::Role
     Properties:
       Description: "Allows a Linux instance to Seamlessly Join the Directory Services Domain"
       AssumeRolePolicyDocument:
         Version: 2012-10-17
         Statement:
           - Effect: Allow
             Principal:
               Service:
               - ec2.amazonaws.com
             Action:
               - sts:AssumeRole
       ManagedPolicyArns:
         - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
         - arn:aws:iam::aws:policy/AmazonSSMDirectoryServiceAccess
       Policies:
         - PolicyName: GetCredentialsFromSecretsManager
           PolicyDocument:
             Version: 2012-10-17
             Statement:
               - Effect: Allow
                 Action:
                   - secretsmanager:GetSecretValue
                   - secretsmanager:DescribeSecret
                 Resource: !Ref DSpasswordInSecretManager

  #-------Instance Profile --------- #
  SeamlessDomainJoinProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref SeamlessDomainJoinRole

  #-------Secret Manager Secret-------#
  DSpasswordInSecretManager:
    Type: 'AWS::SecretsManager::Secret'
    Properties:
      Description: 'Password for DS Administrator User of MSAD'
      Name: 'DSAdminPswd'
      KmsKeyId: !Ref SMKey
      SecretString: !Sub '{"DirectoryId":"${MSAD}","UserName":"Administrator","Password":"${RandomPassword.Data}"}'

  SecretsManagerVpcEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties: 
      #PolicyDocument: Json
      PrivateDnsEnabled: true
      SecurityGroupIds: 
        - !GetAtt BlogVPC.DefaultSecurityGroup
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.secretsmanager'
      SubnetIds: 
        - Ref: 'PrivateSubnet2'
        - Ref: 'PrivateSubnet1'
      VpcEndpointType: Interface
      VpcId: !Ref BlogVPC 

  KmsVpcEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties: 
      #PolicyDocument: Json
      PrivateDnsEnabled: true
      SecurityGroupIds: 
        - !GetAtt BlogVPC.DefaultSecurityGroup
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.kms'
      SubnetIds: 
        - Ref: 'PrivateSubnet2'
        - Ref: 'PrivateSubnet1'
      VpcEndpointType: Interface
      VpcId: !Ref BlogVPC 

  LogsVpcEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties: 
      #PolicyDocument: Json
      PrivateDnsEnabled: true
      SecurityGroupIds: 
        - !GetAtt BlogVPC.DefaultSecurityGroup
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.logs'
      SubnetIds: 
        - Ref: 'PrivateSubnet2'
        - Ref: 'PrivateSubnet1'
      VpcEndpointType: Interface
      VpcId: !Ref BlogVPC 

  #Function to rotate secret, fetch password from Secret Manager and update in the Directory Services Admin User
  SecretsManagerRotationFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: dj-rotate-lambda
      Description: The Lambda function which shall rotate the DS password stored in Secret Manager and updates back in Managed DS
      Handler: sm-rotate-lambda.lambda_handler
      Runtime: python3.7
      Role: !GetAtt 'SecretsManagerRotationFunctionRole.Arn'
      Code:
        S3Bucket: !Ref 'pS3BucketName'
        S3Key: !Sub '${pS3KeySMRotate}'
      Timeout: 30
      Environment:
        Variables:
          SECRETS_MANAGER_ENDPOINT: !Join ['',['https://',!Select [1, !Split [":", !Select [0, !GetAtt SecretsManagerVpcEndpoint.DnsEntries]]]]]
          directoryid:
            Ref: MSAD
      VpcConfig:
        SecurityGroupIds: [!GetAtt BlogVPC.DefaultSecurityGroup]
        SubnetIds: 
          - Ref: 'PrivateSubnet2'
          - Ref: 'PrivateSubnet1'

  #Role required for SecretsManagerRotationFunction Lambda function
  SecretsManagerRotationFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join ['-', [!Ref 'AWS::StackName', !Ref 'AWS::Region', 'rotationfunctionrole']]
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
              - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'          
      Path: "/"
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
      Policies:
      - PolicyName: "cloudwatch-logs-policy"
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogStream
            - logs:PutLogEvents
            - logs:CreateLogGroup
            Resource: "*"
      - PolicyName: "secretsmanager-policy"
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
              - secretsmanager:DescribeSecret
              - secretsmanager:GetSecretValue
              - secretsmanager:PutSecretValue
              - secretsmanager:UpdateSecretVersionStage
            Resource: !Ref DSpasswordInSecretManager
          - Effect: Allow
            Action:
              - secretsmanager:GetRandomPassword
            Resource: "*"
      - PolicyName: "directoryservice-policy"
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
              - ds:DescribeDirectories
            Resource: "*" 
          - Effect: Allow
            Action:
              - ds:ResetUserPassword
            Resource: !Join ['',['arn:aws:ds:',!Ref 'AWS::Region',':',!Ref AWS::AccountId,':directory/',!Ref MSAD]]
      - PolicyName: "kms-policy"
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - kms:DescribeKey
            - kms:Encrypt
            - kms:Decrypt
            - kms:ReEncrypt*
            - kms:GenerateDataKey
            - kms:GenerateDataKeyWithoutPlaintext
            Resource: !GetAtt SMKey.Arn

  KMSAdmin:
    Type: AWS::IAM::Role
    Properties: 
      RoleName: !Join ['-', [!Ref 'AWS::StackName', !Ref 'AWS::Region', 'kms-secrets-manager']]
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Ref 'AWS::AccountId'
            Action:
              - 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/AWSKeyManagementServicePowerUser'

  #Permissions required on Lambda function to invoke 
  LambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Fn::GetAtt:
        - SecretsManagerRotationFunction
        - Arn
      Principal: 'secretsmanager.amazonaws.com'
  
  #Password Rotation Schedule Setup
  DSpasswordInSecretManagerRotationSchedule:
    Type: AWS::SecretsManager::RotationSchedule
    DependsOn: [SecretsManagerRotationFunction, LambdaPermission]
    Properties:
      SecretId: !Ref DSpasswordInSecretManager
      RotationLambdaARN: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:dj-rotate-lambda
      RotationRules:
        AutomaticallyAfterDays: 30 
  SMKeyAlias:
    Type: AWS::KMS::Alias
    Properties: 
      AliasName: alias/kmskey
      TargetKeyId: !Ref SMKey
  SMKey:
    Type: AWS::KMS::Key
    Properties:
      Description: Key used for DS-Admin user SecretPassword Encryption
      EnableKeyRotation: true
      PendingWindowInDays: 7
      KeyPolicy:
        Version: '2012-10-17'
        Id: key-default-1
        Statement:
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: !GetAtt KMSAdmin.Arn
              AWS: !Join ['', ['arn:aws:iam::', !Ref 'AWS::AccountId', ':root']]
            Action: 'kms:*'
            Resource: '*'
          - Sid: Allow use of the key
            Effect: Allow
            Principal:
              Service: 
                - secretsmanager.amazonaws.com
                - lambda.amazonaws.com
            Action:
            - kms:DescribeKey
            - kms:Encrypt
            - kms:Decrypt
            - kms:ReEncrypt*
            - kms:GenerateDataKey
            - kms:GenerateDataKeyWithoutPlaintext
            Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:DSAdminPswd-*'
  RandomPasswordLogGroup:
    Type: AWS::Logs::LogGroup
    Properties: 
      LogGroupName: !Sub /aws/lambda/${AWS::StackName}-RandomStringLambda
      RetentionInDays: 1
  RandomPasswordRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          -
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        -
          PolicyName: Policy
          PolicyDocument:
            Statement:
              -
                Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !GetAtt RandomPasswordLogGroup.Arn

  RandomStringLambda:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: | 
          import cfnresponse
          import secrets
          import string
          
          def handler(event, context):
            alphabet = string.ascii_letters + string.digits
            response_data = {}
            try:
              # Generate an alphanumeric password of the requested length (default: 12)
              length = int(event['ResourceProperties'].get('Length', 12))
              password = ''.join(secrets.choice(alphabet) for _ in range(length))
              response_data['Data'] = password
              cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
            except Exception as e:
              print(e)
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
      Handler: index.handler
      Role: !GetAtt RandomPasswordRole.Arn
      Runtime: python3.7

  RandomPassword:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken: !GetAtt RandomStringLambda.Arn

Outputs:
  DirectoryID:
    Description: ID of the MS Directory
    Value: !Ref MSAD
  PrimaryDNS:
    Description: DNS IPs of the MSAD
    Value: !Select [ '0', !GetAtt MSAD.DnsIpAddresses ]
  SecondaryDNS:
    Description: DNS IPs of the MSAD
    Value: !Select [ '1', !GetAtt MSAD.DnsIpAddresses ]
  SecretARN:
    Description: ID of the Secret in Secret Manager
    Value: !Ref DSpasswordInSecretManager
  RotationLambdaARN:
    Description: The ARN of the rotation lambda
    Value:
      Fn::GetAtt:
      - SecretsManagerRotationFunction
      - Arn