Skip to content

Commit

Permalink
Merge pull request #392 from ustaxcourt/cloudfront-for-documents
Browse files Browse the repository at this point in the history
386: Serve documents from app host.
  • Loading branch information
mmarcotte authored Sep 4, 2020
2 parents 71ec702 + 4f9bb9c commit 4268288
Show file tree
Hide file tree
Showing 13 changed files with 287 additions and 13 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@ web-client/coverage-integration/*
web-client/coverage-unit/*
web-client/pa11y/pa11y-screenshots/*.png
web-client/reports
web-client/terraform/common/cloudfront-edge/index.js.zip
web-client/terraform/common/cloudfront-edge/header-security-lambda.js.zip
web-client/terraform/common/cloudfront-edge/strip-basepath-lambda.js.zip
web-client/tests_output/
42 changes: 42 additions & 0 deletions iam/terraform/environment-specific/main/strip-basepath.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
resource "aws_iam_role" "strip_basepath_lambda_role" {
name = "strip_basepath_lambda_role_${var.environment}"

assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": ["lambda.amazonaws.com", "edgelambda.amazonaws.com"]
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}


resource "aws_iam_role_policy" "strip_basepath_lambda_policy" {
name = "strip_basepath_lambda_policy_${var.environment}"
role = aws_iam_role.strip_basepath_lambda_role.id

policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
EOF
}
6 changes: 6 additions & 0 deletions shared/src/business/test/createTestApplicationContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ const {
const {
deleteWorkItemFromInbox,
} = require('../../persistence/dynamo/workitems/deleteWorkItemFromInbox');
const {
documentUrlTranslator,
} = require('../../../src/business/utilities/documentUrlTranslator');
const {
formatAttachments,
} = require('../../../src/business/utilities/formatAttachments');
Expand Down Expand Up @@ -387,11 +390,14 @@ const createTestApplicationContext = ({ user } = {}) => {
.fn()
.mockImplementation(() => new Uint8Array([])),
docketNumberGenerator: mockCreateDocketNumberGenerator,
documentUrlTranslator: jest.fn().mockImplementation(documentUrlTranslator),
environment: {
appEndpoint: 'localhost:1234',
stage: 'local',
tempDocumentsBucketName: 'MockDocumentBucketName',
},
filterCaseMetadata: jest.fn(),
getAppEndpoint: () => 'localhost:1234',
getBaseUrl: () => 'http://localhost',
getCaseTitle: jest.fn().mockImplementation(Case.getCaseTitle),
getChiefJudgeNameForSigning: jest.fn(),
Expand Down
26 changes: 26 additions & 0 deletions shared/src/business/utilities/documentUrlTranslator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
*
* @params {object} params the params object
* @param {string} documentUrl URL to the document in regionalEndpoint.com/bucket/path format
* @param {boolean} useTempBucket If the document is in the temporary documents or not
* @param {object} applicationContext the application context
*
* @returns {string} the translated URL
*/
exports.documentUrlTranslator = ({
applicationContext,
documentUrl,
useTempBucket,
}) => {
if (applicationContext.environment.stage === 'local') {
return documentUrl;
}

const url = new URL(documentUrl);
const path = useTempBucket ? 'temp-documents' : 'documents';

url.host = applicationContext.getAppEndpoint();
url.pathname = [path, ...url.pathname.split('/').slice(2)].join('/');

return url.toString();
};
47 changes: 47 additions & 0 deletions shared/src/business/utilities/documentUrlTranslator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const { applicationContext } = require('../test/createTestApplicationContext');
const { documentUrlTranslator } = require('./documentUrlTranslator');

describe('documentUrlTranslator', () => {
const documentUrl =
'https://s3.region-name.amazonaws.com/bucketName/documentPath?AWSAccessKeyId=KeyId&Expires=999&Signature=SignatureString';

test('the original url is returned when the environment’s stage is local', () => {
applicationContext.environment.stage = 'local';

const translatedUrl = documentUrlTranslator({
applicationContext,
documentUrl,
useTempBucket: false,
});

expect(translatedUrl).toBe(documentUrl);
});

test('temporary documents are rewritten to the app host’s cloudfront temp-documents endpoint', () => {
applicationContext.environment.stage = 'prod';

const translatedUrl = documentUrlTranslator({
applicationContext,
documentUrl,
useTempBucket: true,
});

const expectedUrl =
'https://localhost:1234/temp-documents/documentPath?AWSAccessKeyId=KeyId&Expires=999&Signature=SignatureString';
expect(translatedUrl).toBe(expectedUrl);
});

test('non-temporary documents are rewritten to the app host’s cloudfront document endpoint', () => {
applicationContext.environment.stage = 'prod';

const translatedUrl = documentUrlTranslator({
applicationContext,
documentUrl,
useTempBucket: false,
});

const expectedUrl =
'https://localhost:1234/documents/documentPath?AWSAccessKeyId=KeyId&Expires=999&Signature=SignatureString';
expect(translatedUrl).toBe(expectedUrl);
});
});
14 changes: 10 additions & 4 deletions shared/src/persistence/s3/getDownloadPolicyUrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ exports.getDownloadPolicyUrl = ({
documentId,
useTempBucket,
}) => {
const bucketName = useTempBucket
? applicationContext.getTempDocumentsBucketName()
: applicationContext.getDocumentsBucketName();

return new Promise((resolve, reject) => {
applicationContext.getStorageClient().getSignedUrl(
'getObject',
{
Bucket: useTempBucket
? applicationContext.getTempDocumentsBucketName()
: applicationContext.getDocumentsBucketName(),
Bucket: bucketName,
Expires: 120,
Key: documentId,
},
Expand All @@ -25,7 +27,11 @@ exports.getDownloadPolicyUrl = ({
return reject(new Error(err));
}
resolve({
url: data,
url: applicationContext.documentUrlTranslator({
applicationContext,
documentUrl: data,
useTempBucket,
}),
});
},
);
Expand Down
6 changes: 5 additions & 1 deletion shared/src/persistence/s3/getPublicDownloadPolicyUrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ exports.getPublicDownloadPolicyUrl = ({ applicationContext, documentId }) => {
return reject(err);
}
resolve({
url: data,
url: applicationContext.documentUrlTranslator({
applicationContext,
documentUrl: data,
useTempBucket: false,
}),
});
},
);
Expand Down
10 changes: 10 additions & 0 deletions web-api/src/applicationContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ const {
const {
deleteWorkItemFromSection,
} = require('../../shared/src/persistence/dynamo/workitems/deleteWorkItemFromSection');
const {
documentUrlTranslator,
} = require('../../shared/src/business/utilities/documentUrlTranslator');
const {
fetchPendingItems,
} = require('../../shared/src/business/useCaseHelper/pendingItems/fetchPendingItems');
Expand Down Expand Up @@ -971,6 +974,9 @@ const {
const execPromise = util.promisify(exec);

const environment = {
appEndpoint: process.env.EFCMS_DOMAIN
? `app.${process.env.EFCMS_DOMAIN}`
: 'localhost:1234',
documentsBucketName: process.env.DOCUMENTS_BUCKET_NAME || '',
dynamoDbEndpoint: process.env.DYNAMODB_ENDPOINT || 'http://localhost:8000',
elasticsearchEndpoint:
Expand Down Expand Up @@ -1201,7 +1207,11 @@ module.exports = appContextUser => {
return {
barNumberGenerator,
docketNumberGenerator,
documentUrlTranslator,
environment,
getAppEndpoint: () => {
return environment.appEndpoint;
},
getCaseTitle: Case.getCaseTitle,
getChromiumBrowser,
getCognito: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ exports.handler = (event, context, callback) => {
`style-src 'self' 'unsafe-inline' ${dynamsoftUrl}`,
`img-src ${applicationUrl} ${subdomainsUrl} data:`,
`font-src ${applicationUrl} ${subdomainsUrl}`,
`frame-src ${s3Url} blob:`,
`frame-src ${s3Url} ${subdomainsUrl} blob:`,
"frame-ancestors 'none'",
];
headers['content-security-policy'] = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Removes the base path (the top-level folder) from the request URL.
*
* @param {object} event the AWS event object
* @param {object} context the context
* @param {object} callback the callback
*/
exports.handler = (event, context, callback) => {
const { request } = event.Records[0].cf;
request.uri = `/${[...request.uri.split('/').slice(2)].join('/')}`;
callback(null, request);
};
104 changes: 104 additions & 0 deletions web-client/terraform/common/frontend.tf
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,58 @@ resource "aws_cloudfront_distribution" "distribution" {
}
}

origin_group {
origin_id = "group-documents.${var.dns_domain}"

failover_criteria {
status_codes = [403, 404, 500, 502, 503, 504]
}

member {
origin_id = "primary-documents.${var.dns_domain}"
}

member {
origin_id = "failover-documents.${var.dns_domain}"
}
}

origin_group {
origin_id = "group-temp-documents.${var.dns_domain}"

failover_criteria {
status_codes = [403, 404, 500, 502, 503, 504]
}

member {
origin_id = "primary-temp-documents.${var.dns_domain}"
}

member {
origin_id = "failover-temp-documents.${var.dns_domain}"
}
}

origin {
domain_name = "${var.dns_domain}-documents-${var.environment}-us-east-1.s3.amazonaws.com"
origin_id = "primary-documents.${var.dns_domain}"
}

origin {
domain_name = "${var.dns_domain}-documents-${var.environment}-us-west-1.s3.amazonaws.com"
origin_id = "failover-documents.${var.dns_domain}"
}

origin {
domain_name = "${var.dns_domain}-temp-documents-${var.environment}-us-east-1.s3.amazonaws.com"
origin_id = "primary-temp-documents.${var.dns_domain}"
}

origin {
domain_name = "${var.dns_domain}-temp-documents-${var.environment}-us-west-1.s3.amazonaws.com"
origin_id = "failover-temp-documents.${var.dns_domain}"
}

custom_error_response {
error_caching_min_ttl = 0
error_code = 404
Expand Down Expand Up @@ -198,6 +250,58 @@ resource "aws_cloudfront_distribution" "distribution" {
viewer_protocol_policy = "redirect-to-https"
}

ordered_cache_behavior {
path_pattern = "/documents/*"
viewer_protocol_policy = "redirect-to-https"
compress = true
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "group-documents.${var.dns_domain}"
min_ttl = 0
default_ttl = 0
max_ttl = 0

lambda_function_association {
event_type = "origin-request"
lambda_arn = aws_lambda_function.strip_basepath_lambda.qualified_arn
include_body = false
}

forwarded_values {
query_string = true

cookies {
forward = "none"
}
}
}

ordered_cache_behavior {
path_pattern = "/temp-documents/*"
viewer_protocol_policy = "redirect-to-https"
compress = true
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "group-temp-documents.${var.dns_domain}"
min_ttl = 0
default_ttl = 0
max_ttl = 0

lambda_function_association {
event_type = "origin-request"
lambda_arn = aws_lambda_function.strip_basepath_lambda.qualified_arn
include_body = false
}

forwarded_values {
query_string = true

cookies {
forward = "none"
}
}
}

aliases = ["app.${var.dns_domain}"]

restrictions {
Expand Down
12 changes: 6 additions & 6 deletions web-client/terraform/common/header-security-lambda.tf
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
data "aws_caller_identity" "current" {}

data "archive_file" "zip_cloudfront_edge" {
data "archive_file" "zip_header_security_lambda" {
type = "zip"
source_file = "${path.module}/cloudfront-edge/index.js"
output_path = "${path.module}/cloudfront-edge/index.js.zip"
source_file = "${path.module}/cloudfront-edge/header-security-lambda.js"
output_path = "${path.module}/cloudfront-edge/header-security-lambda.js.zip"
}

resource "aws_lambda_function" "header_security_lambda" {
filename = data.archive_file.zip_cloudfront_edge.output_path
filename = data.archive_file.zip_header_security_lambda.output_path
function_name = "header_security_lambda_${var.environment}"
role = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/header_security_lambda_role_${var.environment}"
handler = "index.handler"
source_code_hash = data.archive_file.zip_cloudfront_edge.output_base64sha256
handler = "header-security-lambda.handler"
source_code_hash = data.archive_file.zip_header_security_lambda.output_base64sha256
publish = true

runtime = "nodejs10.x"
Expand Down
Loading

0 comments on commit 4268288

Please sign in to comment.