@serverless/aws-cloudfront
Advanced tools
Comparing version 2.0.0 to 3.0.0
@@ -22,2 +22,10 @@ const promisifyMock = (mockFn) => { | ||
const mockCreateCloudFrontOriginAccessIdentity = jest.fn() | ||
const mockCreateCloudFrontOriginAccessIdentityPromise = promisifyMock( | ||
mockCreateCloudFrontOriginAccessIdentity | ||
) | ||
const mockPutBucketPolicy = jest.fn() | ||
const mockPutBucketPolicyPromise = promisifyMock(mockPutBucketPolicy) | ||
module.exports = { | ||
@@ -28,2 +36,6 @@ mockCreateDistribution, | ||
mockDeleteDistribution, | ||
mockCreateCloudFrontOriginAccessIdentity, | ||
mockPutBucketPolicy, | ||
mockPutBucketPolicyPromise, | ||
mockCreateDistributionPromise, | ||
@@ -33,2 +45,3 @@ mockUpdateDistributionPromise, | ||
mockDeleteDistributionPromise, | ||
mockCreateCloudFrontOriginAccessIdentityPromise, | ||
@@ -39,4 +52,9 @@ CloudFront: jest.fn(() => ({ | ||
getDistributionConfig: mockGetDistributionConfig, | ||
deleteDistribution: mockDeleteDistribution | ||
deleteDistribution: mockDeleteDistribution, | ||
createCloudFrontOriginAccessIdentity: mockCreateCloudFrontOriginAccessIdentity | ||
})), | ||
S3: jest.fn(() => ({ | ||
putBucketPolicy: mockPutBucketPolicy | ||
})) | ||
} |
@@ -24,4 +24,10 @@ const { createComponent, assertHasOrigin } = require('../test-utils') | ||
it('creates distribution with custom url origin', async () => { | ||
it('creates distribution with custom url origin and sets defaults', async () => { | ||
await component.default({ | ||
defaults: { | ||
ttl: 10, | ||
'lambda@edge': { | ||
'origin-request': 'arn:aws:lambda:us-east-1:123:function:originRequestFunction' | ||
} | ||
}, | ||
origins: ['https://mycustomorigin.com'] | ||
@@ -28,0 +34,0 @@ }) |
@@ -6,3 +6,5 @@ const { | ||
mockGetDistributionConfigPromise, | ||
mockUpdateDistributionPromise | ||
mockUpdateDistributionPromise, | ||
mockCreateCloudFrontOriginAccessIdentityPromise, | ||
mockPutBucketPolicy | ||
} = require('aws-sdk') | ||
@@ -12,3 +14,3 @@ | ||
describe('Input origin as an S3 bucket url', () => { | ||
describe('S3 origins', () => { | ||
let component | ||
@@ -26,53 +28,154 @@ | ||
it('creates distribution with S3 origin', async () => { | ||
await component.default({ | ||
origins: ['https://mybucket.s3.amazonaws.com'] | ||
describe('When origin is an S3 bucket URL', () => { | ||
it('creates distribution', async () => { | ||
await component.default({ | ||
origins: ['https://mybucket.s3.amazonaws.com'] | ||
}) | ||
assertHasOrigin(mockCreateDistribution, { | ||
Id: 'mybucket', | ||
DomainName: 'mybucket.s3.amazonaws.com', | ||
S3OriginConfig: { | ||
OriginAccessIdentity: '' | ||
}, | ||
CustomHeaders: { | ||
Quantity: 0, | ||
Items: [] | ||
}, | ||
OriginPath: '' | ||
}) | ||
expect(mockCreateDistribution.mock.calls[0][0]).toMatchSnapshot() | ||
}) | ||
assertHasOrigin(mockCreateDistribution, { | ||
Id: 'mybucket', | ||
DomainName: 'mybucket.s3.amazonaws.com', | ||
S3OriginConfig: { | ||
OriginAccessIdentity: '' | ||
}, | ||
CustomHeaders: { | ||
Quantity: 0, | ||
Items: [] | ||
}, | ||
OriginPath: '' | ||
it('updates distribution', async () => { | ||
mockGetDistributionConfigPromise.mockResolvedValueOnce({ | ||
ETag: 'etag', | ||
DistributionConfig: { | ||
Origins: { | ||
Items: [] | ||
} | ||
} | ||
}) | ||
mockUpdateDistributionPromise.mockResolvedValueOnce({ | ||
Distribution: { | ||
Id: 'distributionwithS3originupdated' | ||
} | ||
}) | ||
await component.default({ | ||
origins: ['https://mybucket.s3.amazonaws.com'] | ||
}) | ||
await component.default({ | ||
origins: ['https://anotherbucket.s3.amazonaws.com'] | ||
}) | ||
assertHasOrigin(mockUpdateDistribution, { | ||
Id: 'anotherbucket', | ||
DomainName: 'anotherbucket.s3.amazonaws.com' | ||
}) | ||
expect(mockUpdateDistribution.mock.calls[0][0]).toMatchSnapshot() | ||
}) | ||
expect(mockCreateDistribution.mock.calls[0][0]).toMatchSnapshot() | ||
}) | ||
it('updates distribution', async () => { | ||
mockGetDistributionConfigPromise.mockResolvedValueOnce({ | ||
ETag: 'etag', | ||
DistributionConfig: { | ||
Origins: { | ||
Items: [] | ||
describe('When origin is an S3 URL only accessible via CloudFront', () => { | ||
it('creates distribution', async () => { | ||
mockCreateCloudFrontOriginAccessIdentityPromise.mockResolvedValueOnce({ | ||
CloudFrontOriginAccessIdentity: { | ||
Id: 'access-identity-xyz', | ||
S3CanonicalUserId: 's3-canonical-user-id-xyz' | ||
} | ||
} | ||
}) | ||
mockUpdateDistributionPromise.mockResolvedValueOnce({ | ||
Distribution: { | ||
Id: 'distributionwithS3originupdated' | ||
} | ||
}) | ||
}) | ||
await component.default({ | ||
origins: ['https://mybucket.s3.amazonaws.com'] | ||
}) | ||
await component.default({ | ||
origins: [ | ||
{ | ||
url: 'https://mybucket.s3.amazonaws.com', | ||
private: true | ||
} | ||
] | ||
}) | ||
await component.default({ | ||
origins: ['https://anotherbucket.s3.amazonaws.com'] | ||
expect(mockPutBucketPolicy).toBeCalledWith({ | ||
Bucket: 'mybucket', | ||
Policy: expect.stringContaining('"CanonicalUser":"s3-canonical-user-id-xyz"') | ||
}) | ||
assertHasOrigin(mockCreateDistribution, { | ||
Id: 'mybucket', | ||
DomainName: 'mybucket.s3.amazonaws.com', | ||
S3OriginConfig: { | ||
OriginAccessIdentity: 'origin-access-identity/cloudfront/access-identity-xyz' | ||
}, | ||
CustomHeaders: { | ||
Quantity: 0, | ||
Items: [] | ||
}, | ||
OriginPath: '' | ||
}) | ||
expect(mockCreateDistribution.mock.calls[0][0]).toMatchSnapshot() | ||
}) | ||
assertHasOrigin(mockUpdateDistribution, { | ||
Id: 'anotherbucket', | ||
DomainName: 'anotherbucket.s3.amazonaws.com' | ||
it('updates distribution', async () => { | ||
mockCreateCloudFrontOriginAccessIdentityPromise.mockResolvedValue({ | ||
CloudFrontOriginAccessIdentity: { | ||
Id: 'access-identity-xyz', | ||
S3CanonicalUserId: 's3-canonical-user-id-xyz' | ||
} | ||
}) | ||
mockGetDistributionConfigPromise.mockResolvedValueOnce({ | ||
ETag: 'etag', | ||
DistributionConfig: { | ||
Origins: { | ||
Items: [] | ||
} | ||
} | ||
}) | ||
mockUpdateDistributionPromise.mockResolvedValueOnce({ | ||
Distribution: { | ||
Id: 'distributionwithS3originupdated' | ||
} | ||
}) | ||
await component.default({ | ||
origins: [ | ||
{ | ||
url: 'https://mybucket.s3.amazonaws.com', | ||
private: true | ||
} | ||
] | ||
}) | ||
await component.default({ | ||
origins: [ | ||
{ | ||
url: 'https://anotherbucket.s3.amazonaws.com', | ||
private: true | ||
} | ||
] | ||
}) | ||
expect(mockPutBucketPolicy).toBeCalledWith({ | ||
Bucket: 'anotherbucket', | ||
Policy: expect.stringContaining('"CanonicalUser":"s3-canonical-user-id-xyz"') | ||
}) | ||
assertHasOrigin(mockUpdateDistribution, { | ||
Id: 'anotherbucket', | ||
DomainName: 'anotherbucket.s3.amazonaws.com', | ||
S3OriginConfig: { | ||
OriginAccessIdentity: 'origin-access-identity/cloudfront/access-identity-xyz' | ||
}, | ||
OriginPath: '', | ||
CustomHeaders: { Items: [], Quantity: 0 } | ||
}) | ||
expect(mockCreateDistribution.mock.calls[0][0]).toMatchSnapshot() | ||
}) | ||
expect(mockUpdateDistribution.mock.calls[0][0]).toMatchSnapshot() | ||
}) | ||
}) |
@@ -1,3 +0,5 @@ | ||
module.exports = (originId) => { | ||
return { | ||
const addLambdaAtEdgeToCacheBehavior = require('./addLambdaAtEdgeToCacheBehavior') | ||
module.exports = (originId, defaults = {}) => { | ||
const defaultCacheBehavior = { | ||
TargetOriginId: originId, | ||
@@ -34,3 +36,3 @@ ForwardedValues: { | ||
SmoothStreaming: false, | ||
DefaultTTL: 86400, | ||
DefaultTTL: defaults.ttl || 86400, | ||
MaxTTL: 31536000, | ||
@@ -44,2 +46,6 @@ Compress: false, | ||
} | ||
addLambdaAtEdgeToCacheBehavior(defaultCacheBehavior, defaults['lambda@edge']) | ||
return defaultCacheBehavior | ||
} |
const url = require('url') | ||
module.exports = (origin) => { | ||
module.exports = (origin, { originAccessIdentityId = '' }) => { | ||
const originUrl = typeof origin === 'string' ? origin : origin.url | ||
const { hostname } = url.parse(originUrl) | ||
const { hostname, pathname } = url.parse(originUrl) | ||
@@ -15,3 +15,3 @@ const originConfig = { | ||
}, | ||
OriginPath: '' | ||
OriginPath: pathname === '/' ? '' : pathname | ||
} | ||
@@ -24,3 +24,5 @@ | ||
originConfig.S3OriginConfig = { | ||
OriginAccessIdentity: '' | ||
OriginAccessIdentity: originAccessIdentityId | ||
? `origin-access-identity/cloudfront/${originAccessIdentityId}` | ||
: '' | ||
} | ||
@@ -27,0 +29,0 @@ } else { |
const parseInputOrigins = require('./parseInputOrigins') | ||
const getDefaultCacheBehavior = require('./getDefaultCacheBehavior') | ||
const createOriginAccessIdentity = require('./createOriginAccessIdentity') | ||
const grantCloudFrontBucketAccess = require('./grantCloudFrontBucketAccess') | ||
const createCloudFrontDistribution = async (cf, inputs) => { | ||
const servePrivateContentEnabled = (inputs) => | ||
inputs.origins.some((origin) => { | ||
return origin && origin.private === true | ||
}) | ||
const updateBucketsPolicies = async (s3, origins, s3CanonicalUserId) => { | ||
// update bucket policies with cloudfront access | ||
const bucketNames = origins.Items.filter((origin) => origin.S3OriginConfig).map( | ||
(origin) => origin.Id | ||
) | ||
return Promise.all( | ||
bucketNames.map((bucketName) => grantCloudFrontBucketAccess(s3, bucketName, s3CanonicalUserId)) | ||
) | ||
} | ||
const createCloudFrontDistribution = async (cf, s3, inputs) => { | ||
const params = { | ||
@@ -25,8 +43,22 @@ DistributionConfig: { | ||
const { Origins, CacheBehaviors } = parseInputOrigins(inputs.origins) | ||
let originAccessIdentityId | ||
let s3CanonicalUserId | ||
if (servePrivateContentEnabled(inputs)) { | ||
;({ originAccessIdentityId, s3CanonicalUserId } = await createOriginAccessIdentity(cf)) | ||
} | ||
const { Origins, CacheBehaviors } = parseInputOrigins(inputs.origins, { originAccessIdentityId }) | ||
if (s3CanonicalUserId) { | ||
await updateBucketsPolicies(s3, Origins, s3CanonicalUserId) | ||
} | ||
distributionConfig.Origins = Origins | ||
// set first origin declared as the default cache behavior | ||
distributionConfig.DefaultCacheBehavior = getDefaultCacheBehavior(Origins.Items[0].Id) | ||
distributionConfig.DefaultCacheBehavior = getDefaultCacheBehavior( | ||
Origins.Items[0].Id, | ||
inputs.defaults | ||
) | ||
@@ -46,3 +78,3 @@ if (CacheBehaviors) { | ||
const updateCloudFrontDistribution = async (cf, distributionId, inputs) => { | ||
const updateCloudFrontDistribution = async (cf, s3, distributionId, inputs) => { | ||
// Update logic is a bit weird... | ||
@@ -68,5 +100,22 @@ // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFront.html#updateDistribution-property | ||
const { Origins, CacheBehaviors } = parseInputOrigins(inputs.origins) | ||
let s3CanonicalUserId | ||
let originAccessIdentityId | ||
params.DistributionConfig.DefaultCacheBehavior = getDefaultCacheBehavior(Origins.Items[0].Id) | ||
if (servePrivateContentEnabled(inputs)) { | ||
// presumably it's ok to call create origin access identity again | ||
// aws api returns cached copy of what was previously created | ||
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFront.html#createCloudFrontOriginAccessIdentity-property | ||
;({ originAccessIdentityId, s3CanonicalUserId } = await createOriginAccessIdentity(cf)) | ||
} | ||
const { Origins, CacheBehaviors } = parseInputOrigins(inputs.origins, { originAccessIdentityId }) | ||
if (s3CanonicalUserId) { | ||
await updateBucketsPolicies(s3, Origins, s3CanonicalUserId) | ||
} | ||
params.DistributionConfig.DefaultCacheBehavior = getDefaultCacheBehavior( | ||
Origins.Items[0].Id, | ||
inputs.defaults | ||
) | ||
params.DistributionConfig.Origins = Origins | ||
@@ -73,0 +122,0 @@ |
const getOriginConfig = require('./getOriginConfig') | ||
const getCacheBehavior = require('./getCacheBehavior') | ||
const addLambdaAtEdgeToCacheBehavior = require('./addLambdaAtEdgeToCacheBehavior') | ||
const validLambdaTriggers = [ | ||
'viewer-request', | ||
'origin-request', | ||
'origin-response', | ||
'viewer-response' | ||
] | ||
module.exports = (origins) => { | ||
module.exports = (origins, options) => { | ||
const distributionOrigins = { | ||
@@ -15,6 +10,10 @@ Quantity: 0, | ||
} | ||
let distributionCacheBehaviors | ||
const distributionCacheBehaviors = { | ||
Quantity: 0, | ||
Items: [] | ||
} | ||
for (const origin of origins) { | ||
const originConfig = getOriginConfig(origin) | ||
const originConfig = getOriginConfig(origin, options) | ||
@@ -30,25 +29,4 @@ distributionOrigins.Quantity = distributionOrigins.Quantity + 1 | ||
const lambdaAtEdge = pathPatternConfig['lambda@edge'] || {} | ||
addLambdaAtEdgeToCacheBehavior(cacheBehavior, pathPatternConfig['lambda@edge']) | ||
Object.keys(lambdaAtEdge).forEach((eventType) => { | ||
if (!validLambdaTriggers.includes(eventType)) { | ||
throw new Error( | ||
`"${eventType}" is not a valid lambda trigger. See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-cloudfront-trigger-events.html for valid event types.` | ||
) | ||
} | ||
cacheBehavior.LambdaFunctionAssociations.Quantity = | ||
cacheBehavior.LambdaFunctionAssociations.Quantity + 1 | ||
cacheBehavior.LambdaFunctionAssociations.Items.push({ | ||
EventType: eventType, | ||
LambdaFunctionARN: lambdaAtEdge[eventType], | ||
IncludeBody: true | ||
}) | ||
}) | ||
distributionCacheBehaviors = { | ||
Quantity: 0, | ||
Items: [] | ||
} | ||
distributionCacheBehaviors.Quantity = distributionCacheBehaviors.Quantity + 1 | ||
@@ -55,0 +33,0 @@ distributionCacheBehaviors.Items.push(cacheBehavior) |
{ | ||
"name": "@serverless/aws-cloudfront", | ||
"version": "2.0.0", | ||
"version": "3.0.0", | ||
"main": "./serverless.js", | ||
@@ -5,0 +5,0 @@ "publishConfig": { |
@@ -53,2 +53,6 @@ # aws-cloudfront | ||
enabled: true # optional | ||
defaults: # optional | ||
ttl: 15 | ||
lambda@edge: # added to cloudfront default cache behavior | ||
viewer-request: arn:aws:lambda:us-east-1:123:function:myFunc:version | ||
origins: | ||
@@ -90,2 +94,21 @@ - https://my-bucket.s3.amazonaws.com | ||
#### Private S3 Content | ||
To restrict access to content that you serve from S3 you can mark as `private` your S3 origins: | ||
```yml | ||
# serverless.yml | ||
distribution: | ||
component: '@serverless/aws-cloudfront' | ||
inputs: | ||
origins: | ||
- url: https://my-private-bucket.s3.amazonaws.com | ||
private: true | ||
``` | ||
A bucket policy will be added that grants CloudFront with access to the bucket objects. Note that it doesn't remove any existing permissions on the bucket. If users currently have permission to access the files in your bucket using Amazon S3 URLs you will need to manually remove those. | ||
This is documented in more detail here: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html | ||
### 4. Deploy | ||
@@ -92,0 +115,0 @@ |
@@ -33,10 +33,18 @@ const aws = require('aws-sdk') | ||
const s3 = new aws.S3({ | ||
credentials: this.context.credentials.aws, | ||
region: inputs.region | ||
}) | ||
if (this.state.id) { | ||
if (!equals(this.state.origins, inputs.origins)) { | ||
if ( | ||
!equals(this.state.origins, inputs.origins) || | ||
!equals(this.state.defaults, inputs.defaults) | ||
) { | ||
this.context.debug(`Updating CloudFront distribution of ID ${this.state.id}.`) | ||
this.state = await updateCloudFrontDistribution(cf, this.state.id, inputs) | ||
this.state = await updateCloudFrontDistribution(cf, s3, this.state.id, inputs) | ||
} | ||
} else { | ||
this.context.debug(`Creating CloudFront distribution in the ${inputs.region} region.`) | ||
this.state = await createCloudFrontDistribution(cf, inputs) | ||
this.state = await createCloudFrontDistribution(cf, s3, inputs) | ||
} | ||
@@ -46,2 +54,3 @@ | ||
this.state.origins = inputs.origins | ||
this.state.defaults = inputs.defaults | ||
await this.save() | ||
@@ -48,0 +57,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
72926
30
1011
123
0