Repository: cloudcomponents/cdk-constructs (language: python)

Issue:
StaticSite support to root object in Cloudfront distribution to deploy Hugo, Gatsby and statically generated websites
We are trying to deploy a static website using the `StaticSite` construct.
When you host a Hugo website on S3 it functions perfectly but when you move it to CloudFront you likely get the nasty “AccessDenied” error. This is because the behavior of CloudFront’s default root object is quite different from the behavior of Amazon S3 index documents and how it deals with root objects. When you configure an Amazon S3 bucket as a website and specify the index document, Amazon S3 returns the index document even if a user requests a different subdirectory. This is absolutely not how CloudFront works thus you get the “AccessDenied” error.

We found out the following link is suggesting a possible solution [link](https://chrisschuld.com/2020/05/gatsby-hosting-on-cloudfront/) including an additional lambda at edge that includes the default root object to a subdirectory in case it is missing.

Would it be possible to include this feature in this library to enable the deployment of statically generated website (including Hugo, Gatsby, etc)?

Thank you very much in advance!



Discussion / hints:
+1

Fix patch:
diff --git a/packages/cdk-lambda-at-edge-pattern/README.md b/packages/cdk-lambda-at-edge-pattern/README.md
index fe8b7e847..5dc559b2d 100644
--- a/packages/cdk-lambda-at-edge-pattern/README.md
+++ b/packages/cdk-lambda-at-edge-pattern/README.md
@@ -100,6 +100,30 @@ new cloudfront.CloudFrontWebDistribution(this, 'MyDistribution', {
  });
 ```
 
+### HttpHeaders
+```typescript
+const httpHeaders = new HttpHeaders(this, 'HttpHeaders', {
+  httpHeaders: {
+    'Content-Security-Policy':
+      "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self'",
+    'Strict-Transport-Security':
+      'max-age=31536000; includeSubdomains; preload',
+    'Referrer-Policy': 'same-origin',
+    'X-XSS-Protection': '1; mode=block',
+    'X-Frame-Options': 'DENY',
+    'X-Content-Type-Options': 'nosniff',
+    'Cache-Control': 'no-cache',
+  },
+});
+```
+
+### OriginMutation
+https://chrisschuld.com/2020/05/gatsby-hosting-on-cloudfront/
+
+```typescript
+const originMutation = new OriginMutation(stack, 'OriginMutation');
+```
+
 ## API Reference
 
 See [API.md](https://github.com/cloudcomponents/cdk-constructs/tree/master/packages/cdk-lambda-at-edge-pattern/API.md).
diff --git a/packages/cdk-lambda-at-edge-pattern/src/lambdas/origin-mutation/index.ts b/packages/cdk-lambda-at-edge-pattern/src/lambdas/origin-mutation/index.ts
new file mode 100644
index 000000000..625ec8aa6
--- /dev/null
+++ b/packages/cdk-lambda-at-edge-pattern/src/lambdas/origin-mutation/index.ts
@@ -0,0 +1,27 @@
+import type { CloudFrontRequestHandler } from 'aws-lambda';
+import { getConfig, Config } from '../shared';
+
+type OriginMutationConfig = Config<unknown>;
+
+let CONFIG: OriginMutationConfig;
+
+export const handler: CloudFrontRequestHandler = async (event) => {
+  if (!CONFIG) {
+    CONFIG = getConfig();
+  }
+
+  CONFIG.logger.debug(event);
+
+  const request = event.Records[0].cf.request;
+  const uri = request.uri;
+
+  if (uri.endsWith('/')) {
+    request.uri += 'index.html';
+  } else if (!uri.includes('.')) {
+    request.uri += '/index.html';
+  }
+
+  CONFIG.logger.debug('Returning request:\n', request);
+
+  return request;
+};
diff --git a/packages/cdk-lambda-at-edge-pattern/src/origin-mutation.ts b/packages/cdk-lambda-at-edge-pattern/src/origin-mutation.ts
new file mode 100644
index 000000000..43824be23
--- /dev/null
+++ b/packages/cdk-lambda-at-edge-pattern/src/origin-mutation.ts
@@ -0,0 +1,25 @@
+import * as path from 'path';
+import { LambdaEdgeEventType } from '@aws-cdk/aws-cloudfront';
+import { Code } from '@aws-cdk/aws-lambda';
+import { Construct } from '@aws-cdk/core';
+
+import { EdgeFunction, CommonEdgeFunctionProps } from './edge-function';
+import { LogLevel } from './with-configuration';
+
+export interface OriginMutationProps extends CommonEdgeFunctionProps {
+  readonly logLevel?: LogLevel;
+}
+
+export class OriginMutation extends EdgeFunction {
+  constructor(scope: Construct, id: string, props: OriginMutationProps = {}) {
+    super(scope, id, {
+      name: 'origin-mutation',
+      code: Code.fromAsset(path.join(__dirname, 'lambdas', 'origin-mutation')),
+      eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
+      edgeRole: props.edgeRole,
+      configuration: {
+        logLevel: props.logLevel ?? LogLevel.WARN,
+      },
+    });
+  }
+}


Test patch:
diff --git a/packages/cdk-lambda-at-edge-pattern/src/__tests__/__snapshots__/origin-mutation.test.ts.snap b/packages/cdk-lambda-at-edge-pattern/src/__tests__/__snapshots__/origin-mutation.test.ts.snap
new file mode 100644
index 000000000..20d7acc85
--- /dev/null
+++ b/packages/cdk-lambda-at-edge-pattern/src/__tests__/__snapshots__/origin-mutation.test.ts.snap
@@ -0,0 +1,293 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`default setup: us-east-1 1`] = `
+Object {
+  "Parameters": Any<Object>,
+  "Resources": Object {
+    "AWS679f53fac002430cb0da5b7982bd22872D164C4C": Object {
+      "DependsOn": Array [
+        "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2",
+      ],
+      "Properties": Object {
+        "Code": Any<Object>,
+        "Handler": "index.handler",
+        "Role": Object {
+          "Fn::GetAtt": Array [
+            "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2",
+            "Arn",
+          ],
+        },
+        "Runtime": "nodejs12.x",
+        "Timeout": 120,
+      },
+      "Type": "AWS::Lambda::Function",
+    },
+    "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": Object {
+      "Properties": Object {
+        "AssumeRolePolicyDocument": Object {
+          "Statement": Array [
+            Object {
+              "Action": "sts:AssumeRole",
+              "Effect": "Allow",
+              "Principal": Object {
+                "Service": "lambda.amazonaws.com",
+              },
+            },
+          ],
+          "Version": "2012-10-17",
+        },
+        "ManagedPolicyArns": Array [
+          Object {
+            "Fn::Join": Array [
+              "",
+              Array [
+                "arn:",
+                Object {
+                  "Ref": "AWS::Partition",
+                },
+                ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
+              ],
+            ],
+          },
+        ],
+      },
+      "Type": "AWS::IAM::Role",
+    },
+    "CustomWithConfigurationcloudcomponentscdklambdaatedgepatternwithconfigurationE415FB9B": Object {
+      "DependsOn": Array [
+        "CustomWithConfigurationcloudcomponentscdklambdaatedgepatternwithconfigurationServiceRoleDefaultPolicyA42C156A",
+        "CustomWithConfigurationcloudcomponentscdklambdaatedgepatternwithconfigurationServiceRoleF04EA3EE",
+      ],
+      "Properties": Object {
+        "Code": Any<Object>,
+        "Handler": "index.handler",
+        "Role": Object {
+          "Fn::GetAtt": Array [
+            "CustomWithConfigurationcloudcomponentscdklambdaatedgepatternwithconfigurationServiceRoleF04EA3EE",
+            "Arn",
+          ],
+        },
+        "Runtime": "nodejs12.x",
+        "Timeout": 300,
+      },
+      "Type": "AWS::Lambda::Function",
+    },
+    "CustomWithConfigurationcloudcomponentscdklambdaatedgepatternwithconfigurationServiceRoleDefaultPolicyA42C156A": Object {
+      "Properties": Object {
+        "PolicyDocument": Object {
+          "Statement": Array [
+            Object {
+              "Action": Array [
+                "lambda:GetFunction",
+                "lambda:UpdateFunctionCode",
+              ],
+              "Effect": "Allow",
+              "Resource": Object {
+                "Fn::GetAtt": Array [
+                  "originmutationProviderA4786C4E",
+                  "Parameter.Value",
+                ],
+              },
+            },
+          ],
+          "Version": "2012-10-17",
+        },
+        "PolicyName": "CustomWithConfigurationcloudcomponentscdklambdaatedgepatternwithconfigurationServiceRoleDefaultPolicyA42C156A",
+        "Roles": Array [
+          Object {
+            "Ref": "CustomWithConfigurationcloudcomponentscdklambdaatedgepatternwithconfigurationServiceRoleF04EA3EE",
+          },
+        ],
+      },
+      "Type": "AWS::IAM::Policy",
+    },
+    "CustomWithConfigurationcloudcomponentscdklambdaatedgepatternwithconfigurationServiceRoleF04EA3EE": Object {
+      "Properties": Object {
+        "AssumeRolePolicyDocument": Object {
+          "Statement": Array [
+            Object {
+              "Action": "sts:AssumeRole",
+              "Effect": "Allow",
+              "Principal": Object {
+                "Service": "lambda.amazonaws.com",
+              },
+            },
+          ],
+          "Version": "2012-10-17",
+        },
+        "ManagedPolicyArns": Array [
+          Object {
+            "Fn::Join": Array [
+              "",
+              Array [
+                "arn:",
+                Object {
+                  "Ref": "AWS::Partition",
+                },
+                ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
+              ],
+            ],
+          },
+        ],
+      },
+      "Type": "AWS::IAM::Role",
+    },
+    "OriginMutationWithConfiguration4D5BA8BC": Object {
+      "DeletionPolicy": "Delete",
+      "Properties": Object {
+        "Configuration": "{
+  \\"logLevel\\": \\"warn\\"
+}",
+        "FunctionName": Object {
+          "Fn::Select": Array [
+            6,
+            Object {
+              "Fn::Split": Array [
+                ":",
+                Object {
+                  "Fn::GetAtt": Array [
+                    "originmutationProviderA4786C4E",
+                    "Parameter.Value",
+                  ],
+                },
+              ],
+            },
+          ],
+        },
+        "Region": "us-east-1",
+        "ServiceToken": Object {
+          "Fn::GetAtt": Array [
+            "CustomWithConfigurationcloudcomponentscdklambdaatedgepatternwithconfigurationE415FB9B",
+            "Arn",
+          ],
+        },
+      },
+      "Type": "Custom::WithConfiguration",
+      "UpdateReplacePolicy": "Delete",
+    },
+    "originmutationFunction689DF756": Object {
+      "DependsOn": Array [
+        "originmutationRoleB8F97561",
+      ],
+      "Properties": Object {
+        "Code": Any<Object>,
+        "Handler": "index.handler",
+        "Role": Object {
+          "Fn::GetAtt": Array [
+            "originmutationRoleB8F97561",
+            "Arn",
+          ],
+        },
+        "Runtime": "nodejs12.x",
+      },
+      "Type": "AWS::Lambda::Function",
+    },
+    "originmutationProviderA4786C4E": Object {
+      "DeletionPolicy": "Delete",
+      "DependsOn": Array [
+        "originmutationProviderCustomResourcePolicyE087AD13",
+      ],
+      "Properties": Object {
+        "Create": Any<Object>,
+        "InstallLatestAwsSdk": true,
+        "ServiceToken": Object {
+          "Fn::GetAtt": Array [
+            "AWS679f53fac002430cb0da5b7982bd22872D164C4C",
+            "Arn",
+          ],
+        },
+        "Update": Any<Object>,
+      },
+      "Type": "Custom::AWS",
+      "UpdateReplacePolicy": "Delete",
+    },
+    "originmutationProviderCustomResourcePolicyE087AD13": Object {
+      "Properties": Object {
+        "PolicyDocument": Object {
+          "Statement": Array [
+            Object {
+              "Action": "ssm:GetParameter",
+              "Effect": "Allow",
+              "Resource": Object {
+                "Fn::Join": Array [
+                  "",
+                  Array [
+                    "arn:",
+                    Object {
+                      "Ref": "AWS::Partition",
+                    },
+                    ":ssm:us-east-1:",
+                    Object {
+                      "Ref": "AWS::AccountId",
+                    },
+                    ":parameter",
+                    Object {
+                      "Ref": "originmutationStringParameterFEABB344",
+                    },
+                  ],
+                ],
+              },
+            },
+          ],
+          "Version": "2012-10-17",
+        },
+        "PolicyName": "originmutationProviderCustomResourcePolicyE087AD13",
+        "Roles": Array [
+          Object {
+            "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2",
+          },
+        ],
+      },
+      "Type": "AWS::IAM::Policy",
+    },
+    "originmutationRoleB8F97561": Object {
+      "Properties": Object {
+        "AssumeRolePolicyDocument": Object {
+          "Statement": Array [
+            Object {
+              "Action": "sts:AssumeRole",
+              "Effect": "Allow",
+              "Principal": Object {
+                "Service": Array [
+                  "edgelambda.amazonaws.com",
+                  "lambda.amazonaws.com",
+                ],
+              },
+            },
+          ],
+          "Version": "2012-10-17",
+        },
+        "ManagedPolicyArns": Array [
+          Object {
+            "Fn::Join": Array [
+              "",
+              Array [
+                "arn:",
+                Object {
+                  "Ref": "AWS::Partition",
+                },
+                ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
+              ],
+            ],
+          },
+        ],
+      },
+      "Type": "AWS::IAM::Role",
+    },
+    "originmutationStringParameterFEABB344": Object {
+      "Properties": Object {
+        "Description": "Parameter stored for cross region Lambda@Edge",
+        "Name": "/cloudcomponents/edge-lambda/Dummy/origin-mutation/c83d99d8def072b82738c0d40222a792527c17f94d",
+        "Type": "String",
+        "Value": Object {
+          "Fn::GetAtt": Array [
+            "originmutationFunction689DF756",
+            "Arn",
+          ],
+        },
+      },
+      "Type": "AWS::SSM::Parameter",
+    },
+  },
+}
+`;
diff --git a/packages/cdk-lambda-at-edge-pattern/src/__tests__/origin-mutation.test.ts b/packages/cdk-lambda-at-edge-pattern/src/__tests__/origin-mutation.test.ts
new file mode 100644
index 000000000..8df15dae2
--- /dev/null
+++ b/packages/cdk-lambda-at-edge-pattern/src/__tests__/origin-mutation.test.ts
@@ -0,0 +1,37 @@
+import { App, Stack } from '@aws-cdk/core';
+import 'jest-cdk-snapshot';
+
+import { OriginMutation } from '../origin-mutation';
+
+test('default setup: us-east-1', () => {
+  // GIVEN
+  const app = new App();
+
+  const stack = new Stack(app, 'Dummy', {
+    env: {
+      region: 'us-east-1',
+    },
+  });
+
+  // WHEN
+  new OriginMutation(stack, 'OriginMutation');
+
+  // THEN
+  const supportStack = app.node.tryFindChild(`lambda-at-edge-support-stack`);
+
+  expect(supportStack).toBeUndefined();
+
+  expect(stack).toMatchCdkSnapshot({
+    ignoreAssets: true,
+    propertyMatchers: {
+      Resources: {
+        originmutationProviderA4786C4E: {
+          Properties: {
+            Create: expect.any(Object),
+            Update: expect.any(Object),
+          },
+        },
+      },
+    },
+  });
+});