Repository: RenovoSolutions/cdk-library-certbot (language: python)

Issue:
Needs permissions for ACM certificates to be listed and uploaded
Missed this when converting this from Terraform plan. Need to add ACM IAM perms.

Example from Terraform:
```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "acm:ListCertificates",
                "acm:ImportCertificate"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "acm:DescribeCertificate"
            ],
            "Resource": [
                "arn:aws:acm:${data.aws_region.current.name}:${data.aws_caller_identity.default.account_id}:certificate/*"
            ]
        }
    ]
}
```


Discussion / hints:

Fix patch:
diff --git a/.projen/deps.json b/.projen/deps.json
index d654e3ea..aceed212 100644
--- a/.projen/deps.json
+++ b/.projen/deps.json
@@ -99,16 +99,36 @@
       "version": "^1.127.0",
       "type": "peer"
     },
+    {
+      "name": "@aws-cdk/aws-iam",
+      "version": "^1.127.0",
+      "type": "peer"
+    },
     {
       "name": "@aws-cdk/aws-lambda",
       "version": "^1.127.0",
       "type": "peer"
     },
+    {
+      "name": "@aws-cdk/aws-route53",
+      "version": "^1.127.0",
+      "type": "peer"
+    },
     {
       "name": "@aws-cdk/aws-s3",
       "version": "^1.127.0",
       "type": "peer"
     },
+    {
+      "name": "@aws-cdk/aws-sns-subscriptions",
+      "version": "^1.127.0",
+      "type": "peer"
+    },
+    {
+      "name": "@aws-cdk/aws-sns",
+      "version": "^1.127.0",
+      "type": "peer"
+    },
     {
       "name": "@aws-cdk/core",
       "version": "^1.127.0",
@@ -129,16 +149,36 @@
       "version": "^1.127.0",
       "type": "runtime"
     },
+    {
+      "name": "@aws-cdk/aws-iam",
+      "version": "^1.127.0",
+      "type": "runtime"
+    },
     {
       "name": "@aws-cdk/aws-lambda",
       "version": "^1.127.0",
       "type": "runtime"
     },
+    {
+      "name": "@aws-cdk/aws-route53",
+      "version": "^1.127.0",
+      "type": "runtime"
+    },
     {
       "name": "@aws-cdk/aws-s3",
       "version": "^1.127.0",
       "type": "runtime"
     },
+    {
+      "name": "@aws-cdk/aws-sns-subscriptions",
+      "version": "^1.127.0",
+      "type": "runtime"
+    },
+    {
+      "name": "@aws-cdk/aws-sns",
+      "version": "^1.127.0",
+      "type": "runtime"
+    },
     {
       "name": "@aws-cdk/core",
       "version": "^1.127.0",
diff --git a/.projen/tasks.json b/.projen/tasks.json
index 6e7147a8..3e48f890 100644
--- a/.projen/tasks.json
+++ b/.projen/tasks.json
@@ -296,7 +296,7 @@
           "exec": "yarn install --check-files"
         },
         {
-          "exec": "yarn upgrade @aws-cdk/assert @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-import-resolver-node eslint-import-resolver-typescript eslint-plugin-import jest jest-junit jsii jsii-diff jsii-docgen jsii-pacmak json-schema npm-check-updates standard-version ts-jest typescript @aws-cdk/aws-events-targets @aws-cdk/aws-events @aws-cdk/aws-lambda @aws-cdk/aws-s3 @aws-cdk/core constructs @aws-cdk/aws-events-targets @aws-cdk/aws-events @aws-cdk/aws-lambda @aws-cdk/aws-s3 @aws-cdk/core"
+          "exec": "yarn upgrade @aws-cdk/assert @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-import-resolver-node eslint-import-resolver-typescript eslint-plugin-import jest jest-junit jsii jsii-diff jsii-docgen jsii-pacmak json-schema npm-check-updates standard-version ts-jest typescript @aws-cdk/aws-events-targets @aws-cdk/aws-events @aws-cdk/aws-iam @aws-cdk/aws-lambda @aws-cdk/aws-route53 @aws-cdk/aws-s3 @aws-cdk/aws-sns-subscriptions @aws-cdk/aws-sns @aws-cdk/core constructs @aws-cdk/aws-events-targets @aws-cdk/aws-events @aws-cdk/aws-iam @aws-cdk/aws-lambda @aws-cdk/aws-route53 @aws-cdk/aws-s3 @aws-cdk/aws-sns-subscriptions @aws-cdk/aws-sns @aws-cdk/core"
         },
         {
           "exec": "npx projen"
diff --git a/.projenrc.js b/.projenrc.js
index cb434444..1ff4e51d 100644
--- a/.projenrc.js
+++ b/.projenrc.js
@@ -13,6 +13,10 @@ const project = new AwsCdkConstructLibrary({
     '@aws-cdk/aws-s3',
     '@aws-cdk/aws-events',
     '@aws-cdk/aws-events-targets',
+    '@aws-cdk/aws-iam',
+    '@aws-cdk/aws-sns',
+    '@aws-cdk/aws-sns-subscriptions',
+    '@aws-cdk/aws-route53',
   ],
   projectType: ProjectType.LIB,
   releaseToNpm: true,
diff --git a/API.md b/API.md
index 99e33507..206a3797 100644
--- a/API.md
+++ b/API.md
@@ -56,6 +56,20 @@ public readonly handler: Function;
 
 #### Properties <a name="Properties"></a>
 
+##### `hostedZoneNames`<sup>Required</sup> <a name="@renovosolutions/cdk-library-certbot.ICertbotProps.property.hostedZoneNames"></a>
+
+```typescript
+public readonly hostedZoneNames: string[];
+```
+
+- *Type:* `string`[]
+
+Any additional Lambda layers to use with the created function.
+
+For example Lambda Extensions
+
+---
+
 ##### `letsencryptDomains`<sup>Required</sup> <a name="@renovosolutions/cdk-library-certbot.ICertbotProps.property.letsencryptDomains"></a>
 
 ```typescript
@@ -96,17 +110,41 @@ If no bucket is given one will be created automatically.
 
 ---
 
-##### `layers`<sup>Optional</sup> <a name="@renovosolutions/cdk-library-certbot.ICertbotProps.property.layers"></a>
+##### `enableInsights`<sup>Optional</sup> <a name="@renovosolutions/cdk-library-certbot.ICertbotProps.property.enableInsights"></a>
 
 ```typescript
-public readonly layers: LayerVersion[];
+public readonly enableInsights: boolean;
 ```
 
-- *Type:* [`@aws-cdk/aws-lambda.LayerVersion`](#@aws-cdk/aws-lambda.LayerVersion)[]
+- *Type:* `boolean`
 
-Any additional Lambda layers to use with the created function.
+Whether or not to enable Lambda Insights.
 
-For example Lambda Extensions
+---
+
+##### `insightsARN`<sup>Optional</sup> <a name="@renovosolutions/cdk-library-certbot.ICertbotProps.property.insightsARN"></a>
+
+```typescript
+public readonly insightsARN: string;
+```
+
+- *Type:* `string`
+
+Insights layer ARN for your region.
+
+Defaults to US-EAST-1
+
+---
+
+##### `layers`<sup>Optional</sup> <a name="@renovosolutions/cdk-library-certbot.ICertbotProps.property.layers"></a>
+
+```typescript
+public readonly layers: ILayerVersion[];
+```
+
+- *Type:* [`@aws-cdk/aws-lambda.ILayerVersion`](#@aws-cdk/aws-lambda.ILayerVersion)[]
+
+Hosted zone names that will be required for DNS verification with certbot.
 
 ---
 
@@ -152,3 +190,15 @@ Default is 30 days
 
 ---
 
+##### `snsTopic`<sup>Optional</sup> <a name="@renovosolutions/cdk-library-certbot.ICertbotProps.property.snsTopic"></a>
+
+```typescript
+public readonly snsTopic: Topic;
+```
+
+- *Type:* [`@aws-cdk/aws-sns.Topic`](#@aws-cdk/aws-sns.Topic)
+
+The SNS topic to notify when a new cert is issued.
+
+---
+
diff --git a/package.json b/package.json
index abf78ac5..773002c5 100644
--- a/package.json
+++ b/package.json
@@ -61,16 +61,24 @@
   "peerDependencies": {
     "@aws-cdk/aws-events": "^1.127.0",
     "@aws-cdk/aws-events-targets": "^1.127.0",
+    "@aws-cdk/aws-iam": "^1.127.0",
     "@aws-cdk/aws-lambda": "^1.127.0",
+    "@aws-cdk/aws-route53": "^1.127.0",
     "@aws-cdk/aws-s3": "^1.127.0",
+    "@aws-cdk/aws-sns": "^1.127.0",
+    "@aws-cdk/aws-sns-subscriptions": "^1.127.0",
     "@aws-cdk/core": "^1.127.0",
     "constructs": "^3.2.27"
   },
   "dependencies": {
     "@aws-cdk/aws-events": "^1.127.0",
     "@aws-cdk/aws-events-targets": "^1.127.0",
+    "@aws-cdk/aws-iam": "^1.127.0",
     "@aws-cdk/aws-lambda": "^1.127.0",
+    "@aws-cdk/aws-route53": "^1.127.0",
     "@aws-cdk/aws-s3": "^1.127.0",
+    "@aws-cdk/aws-sns": "^1.127.0",
+    "@aws-cdk/aws-sns-subscriptions": "^1.127.0",
     "@aws-cdk/core": "^1.127.0"
   },
   "bundledDependencies": [],
diff --git a/src/index.ts b/src/index.ts
index a19ed560..ff6a079a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,8 +1,12 @@
 import * as path from 'path';
 import * as events from '@aws-cdk/aws-events';
 import * as targets from '@aws-cdk/aws-events-targets';
+import * as iam from '@aws-cdk/aws-iam';
 import * as lambda from '@aws-cdk/aws-lambda';
+import * as r53 from '@aws-cdk/aws-route53';
 import * as s3 from '@aws-cdk/aws-s3';
+import * as sns from '@aws-cdk/aws-sns';
+import * as subscriptions from '@aws-cdk/aws-sns-subscriptions';
 import * as cdk from '@aws-cdk/core';
 
 export interface ICertbotProps {
@@ -17,7 +21,11 @@ export interface ICertbotProps {
   /**
    * Any additional Lambda layers to use with the created function. For example Lambda Extensions
    */
-  layers?: lambda.LayerVersion[];
+  hostedZoneNames: string[];
+  /**
+   * Hosted zone names that will be required for DNS verification with certbot
+   */
+  layers?: lambda.ILayerVersion[];
   /**
    * The S3 bucket to place the resulting certificates in. If no bucket is given one will be created automatically.
    */
@@ -34,6 +42,18 @@ export interface ICertbotProps {
    * Set the preferred certificate chain. Default None.
    */
   preferredChain?: string;
+  /**
+   * The SNS topic to notify when a new cert is issued
+   */
+  snsTopic?: sns.Topic;
+  /**
+   * Whether or not to enable Lambda Insights
+   */
+  enableInsights?: boolean;
+  /**
+   * Insights layer ARN for your region. Defaults to US-EAST-1
+   */
+  insightsARN?: string;
 }
 
 export class Certbot extends cdk.Construct {
@@ -58,12 +78,92 @@ export class Certbot extends cdk.Construct {
       });
     }
 
+    const functionDir = path.join(__dirname, '../function');
+
+    if (props.snsTopic === undefined) {
+      props.snsTopic = new sns.Topic(this, 'topic');
+      props.snsTopic.addSubscription(new subscriptions.EmailSubscription(props.letsencryptEmail));
+    }
+
     props.layers = (props.layers === undefined) ? [] : props.layers;
+    props.enableInsights = (props.enableInsights === undefined) ? false : props.enableInsights;
+    props.insightsARN = (props.insightsARN === undefined) ? 'arn:aws:lambda:' + cdk.Stack.of(this).region + ':580247275435:layer:LambdaInsightsExtension:14' : props.insightsARN;
 
-    const functionDir = path.join(__dirname, '../function');
+    let managedPolicies = [iam.ManagedPolicy.fromAwsManagedPolicyName('AWSLambdaBasicExecutionRole')];
+    if (props.enableInsights) {
+      managedPolicies.push(iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLambdaInsightsExecutionRolePolicy'));
+      props.layers.push(lambda.LayerVersion.fromLayerVersionArn(this, 'insightsLayer', props.insightsARN));
+    }
+
+    const snsPolicy = new iam.ManagedPolicy(this, 'snsPolicy', {
+      description: 'Allow the Certbot function to notify an SNS topic upon completion.',
+      statements: [
+        new iam.PolicyStatement({
+          effect: iam.Effect.ALLOW,
+          actions: ['sns:Publish'],
+          resources: [props.snsTopic.topicArn],
+        }),
+      ],
+    });
+
+    let hostedZones = [];
+    for (var domainName in props.hostedZoneNames) {
+      hostedZones.push(r53.HostedZone.fromLookup(this, 'zone' + domainName, {
+        domainName,
+        privateZone: false,
+      }).hostedZoneArn);
+    }
+
+    const r53Policy = new iam.ManagedPolicy(this, 'r53Policy', {
+      description: 'Allow the Certbot function to perform DNS verification.',
+      statements: [
+        new iam.PolicyStatement({
+          effect: iam.Effect.ALLOW,
+          actions: ['route53:ListHostedZones'],
+          resources: ['*'],
+        }),
+        new iam.PolicyStatement({
+          effect: iam.Effect.ALLOW,
+          actions: [
+            'route53:GetChange',
+            'route53:ChangeResourceRecordSets',
+          ],
+          resources: ['arn:aws:route53:::change/*'].concat(hostedZones),
+        }),
+      ],
+    });
+
+    const acmPolicy = new iam.ManagedPolicy(this, 'acmPolicy', {
+      description: 'Allow the Certbot function to import and list certificates.',
+      statements: [
+        new iam.PolicyStatement({
+          effect: iam.Effect.ALLOW,
+          actions: [
+            'acm:ListCertificates',
+            'acm:ImportCertificates',
+          ],
+          resources: ['*'],
+        }),
+        new iam.PolicyStatement({
+          effect: iam.Effect.ALLOW,
+          actions: ['acm:DescribeCertificates'],
+          resources: ['arn:aws:acm:' + cdk.Stack.of(this).region + ':' + cdk.Stack.of(this).account + ':certificate/*'],
+        }),
+      ],
+    });
+
+    managedPolicies.push(snsPolicy);
+    managedPolicies.push(r53Policy);
+    managedPolicies.push(acmPolicy);
+
+    const role = new iam.Role(this, 'role', {
+      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
+      managedPolicies,
+    });
 
     this.handler = new lambda.Function(this, 'handler', {
       runtime: lambda.Runtime.PYTHON_3_8,
+      role,
       code: lambda.Code.fromAsset(functionDir + '/function.zip', {
         // Not working with github actions
         // bundling: {
@@ -101,6 +201,7 @@ export class Certbot extends cdk.Construct {
         OBJECT_PREFIX: (props.objectPrefix === undefined) ? '' : props.objectPrefix,
         REISSUE_DAYS: (props.reIssueDays === undefined) ? '30' : String(props.reIssueDays),
         PREFERRED_CHAIN: (props.preferredChain === undefined) ? 'None' : props.preferredChain,
+        NOTIFICATION_SNS_ARN: props.snsTopic.topicArn,
       },
       layers: props.layers,
     });
diff --git a/yarn.lock b/yarn.lock
index 9231b0ab..939422e8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -375,7 +375,7 @@
     "@aws-cdk/custom-resources" "1.127.0"
     constructs "^3.3.69"
 
-"@aws-cdk/aws-iam@1.127.0":
+"@aws-cdk/aws-iam@1.127.0", "@aws-cdk/aws-iam@^1.127.0":
   version "1.127.0"
   resolved "https://registry.yarnpkg.com/@aws-cdk/aws-iam/-/aws-iam-1.127.0.tgz#04f47e1cd6d71bf05975e400f9bbf100db57388b"
   integrity sha512-JPbf69uj3LmE9nM2Y1vVGO6zMIu9mWGKq3k21PFGLbxExvRiSYrmYFt1qqDNIcSgOubN6kQa8dD4R5j8QPlpDg==
@@ -480,7 +480,7 @@
     "@aws-cdk/region-info" "1.127.0"
     constructs "^3.3.69"
 
-"@aws-cdk/aws-route53@1.127.0":
+"@aws-cdk/aws-route53@1.127.0", "@aws-cdk/aws-route53@^1.127.0":
   version "1.127.0"
   resolved "https://registry.yarnpkg.com/@aws-cdk/aws-route53/-/aws-route53-1.127.0.tgz#5f43b53e764c59b0dff8f6e07ae453dff99685f6"
   integrity sha512-AukjVi0kR6zP1F/0UIHmxfAPGh+ZlJWYokDqUPMjfgJUNpxGkFnkibMcjIG4S/P9TlESrvikF+2eYyjsY7uP8w==
@@ -559,7 +559,7 @@
     "@aws-cdk/core" "1.127.0"
     constructs "^3.3.69"
 
-"@aws-cdk/aws-sns-subscriptions@1.127.0":
+"@aws-cdk/aws-sns-subscriptions@1.127.0", "@aws-cdk/aws-sns-subscriptions@^1.127.0":
   version "1.127.0"
   resolved "https://registry.yarnpkg.com/@aws-cdk/aws-sns-subscriptions/-/aws-sns-subscriptions-1.127.0.tgz#234227e156fd208c8bee7079117f297c209af839"
   integrity sha512-UQMVfvV5YwbuI4KJB7GeELrVlyFnBpnGmehjYJUbf30Q3IBJDe1eOX2nb821hMB6UXRUzD7Jl328YnG75tOrPg==
@@ -572,7 +572,7 @@
     "@aws-cdk/core" "1.127.0"
     constructs "^3.3.69"
 
-"@aws-cdk/aws-sns@1.127.0":
+"@aws-cdk/aws-sns@1.127.0", "@aws-cdk/aws-sns@^1.127.0":
   version "1.127.0"
   resolved "https://registry.yarnpkg.com/@aws-cdk/aws-sns/-/aws-sns-1.127.0.tgz#c3e8a7ec688ccfba94288349a272244f53c84567"
   integrity sha512-y57656L2DT5J3P+xkdGt3bFFDfAJ3oVA2em0jNGFRt1P81snvArGMvCR86AFqY6o5bWi6sFupmtr6Y6JvNKPPw==


Test patch:
diff --git a/test/certbot.test.ts b/test/certbot.test.ts
index 7c0f4e77..1f026d0c 100644
--- a/test/certbot.test.ts
+++ b/test/certbot.test.ts
@@ -4,14 +4,24 @@ import { Certbot } from '../src/index';
 
 test('Default', () => {
   const app = new cdk.App();
-  const stack = new cdk.Stack(app, 'TestStack');
+  const stack = new cdk.Stack(app, 'TestStack', {
+    env: {
+      account: '123456789012', // not a real account
+      region: 'us-east-1',
+    },
+  });
 
   new Certbot(stack, 'Certbot', {
     letsencryptDomains: 'test.local',
     letsencryptEmail: 'test@test.local',
+    hostedZoneNames: ['example.com'],
   });
 
   expectCDK(stack).to(countResources('AWS::Lambda::Function', 1));
   expectCDK(stack).to(countResources('AWS::Events::Rule', 1));
   expectCDK(stack).to(countResources('AWS::S3::Bucket', 1));
+  expectCDK(stack).to(countResources('AWS::IAM::ManagedPolicy', 3));
+  expectCDK(stack).to(countResources('AWS::IAM::Role', 1));
+  expectCDK(stack).to(countResources('AWS::SNS::Topic', 1));
+  expectCDK(stack).to(countResources('AWS::SNS::Subscription', 1));
 });