Skip to content

Commit

Permalink
feat(cloudfront): support geo restrictions for cloudfront distribution (
Browse files Browse the repository at this point in the history
#7345)

- Restrictions currently support GeoRestriction only, but if CloudFront adds support for other types it can easily be expanded
- Closes #3456
  • Loading branch information
mina-asham authored May 7, 2020
1 parent 87860ca commit cf25ba0
Show file tree
Hide file tree
Showing 5 changed files with 356 additions and 2 deletions.
14 changes: 14 additions & 0 deletions packages/@aws-cdk/aws-cloudfront/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,17 @@ See [Importing an SSL/TLS Certificate](https://docs.aws.amazon.com/AmazonCloudFr
Example:

[create a distrubution with an iam certificate example](test/example.iam-cert-alias.lit.ts)

#### Restrictions

CloudFront supports adding restrictions to your distribution.

See [Restricting the Geographic Distribution of Your Content](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/georestrictions.html) in the CloudFront User Guide.

Example:
```ts
new cloudfront.CloudFrontWebDistribution(stack, 'MyDistribution', {
//...
geoRestriction: GeoRestriction.whitelist('US', 'UK')
});
```
71 changes: 71 additions & 0 deletions packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,58 @@ export class ViewerCertificate {
public readonly aliases: string[] = []) { }
}

/**
* Controls the countries in which your content is distributed.
*/
export class GeoRestriction {

/**
* Whitelist specific countries which you want CloudFront to distribute your content.
*
* @param locations Two-letter, uppercase country code for a country
* that you want to whitelist. Include one element for each country.
* See ISO 3166-1-alpha-2 code on the *International Organization for Standardization* website
*/
public static whitelist(...locations: string[]) {
return new GeoRestriction('whitelist', GeoRestriction.validateLocations(locations));
}

/**
* Blacklist specific countries which you don't want CloudFront to distribute your content.
*
* @param locations Two-letter, uppercase country code for a country
* that you want to blacklist. Include one element for each country.
* See ISO 3166-1-alpha-2 code on the *International Organization for Standardization* website
*/
public static blacklist(...locations: string[]) {
return new GeoRestriction('blacklist', GeoRestriction.validateLocations(locations));
}

private static LOCATION_REGEX = /^[A-Z]{2}$/;

private static validateLocations(locations: string[]) {
if (locations.length === 0) {
throw new Error('Should provide at least 1 location');
}
locations.forEach(location => {
if (!GeoRestriction.LOCATION_REGEX.test(location)) {
throw new Error(`Invalid location format for location: ${location}, location should be two-letter and uppercase country ISO 3166-1-alpha-2 code`);
}
});
return locations;
}

/**
* Creates an instance of GeoRestriction for internal use
*
* @param restrictionType Specifies the restriction type to impose (whitelist or blacklist)
* @param locations Two-letter, uppercase country code for a country
* that you want to whitelist/blacklist. Include one element for each country.
* See ISO 3166-1-alpha-2 code on the *International Organization for Standardization* website
*/
private constructor(readonly restrictionType: 'whitelist' | 'blacklist', readonly locations: string[]) {}
}

export interface CloudFrontWebDistributionProps {

/**
Expand Down Expand Up @@ -576,6 +628,13 @@ export interface CloudFrontWebDistributionProps {
* @see https://aws.amazon.com/premiumsupport/knowledge-center/custom-ssl-certificate-cloudfront/
*/
readonly viewerCertificate?: ViewerCertificate;

/**
* Controls the countries in which your content is distributed.
*
* @default No geo restriction
*/
readonly geoRestriction?: GeoRestriction;
}

/**
Expand Down Expand Up @@ -818,6 +877,18 @@ export class CloudFrontWebDistribution extends cdk.Construct implements IDistrib
};
}

if (props.geoRestriction) {
distributionConfig = {
...distributionConfig,
restrictions: {
geoRestriction: {
restrictionType: props.geoRestriction.restrictionType,
locations: props.geoRestriction.locations,
},
},
};
}

const distribution = new CfnDistribution(this, 'CFDistribution', { distributionConfig });
this.node.defaultChild = distribution;
this.domainName = distribution.attrDomainName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"Resources": {
"Bucket83908E77": {
"DeletionPolicy": "Delete",
"UpdateReplacePolicy": "Delete",
"Type": "AWS::S3::Bucket"
},
"MyDistributionCFDistributionDE147309": {
"Type": "AWS::CloudFront::Distribution",
"Properties": {
"DistributionConfig": {
"DefaultCacheBehavior": {
"AllowedMethods": [
"GET",
"HEAD"
],
"CachedMethods": [
"GET",
"HEAD"
],
"ForwardedValues": {
"Cookies": {
"Forward": "none"
},
"QueryString": false
},
"TargetOriginId": "origin1",
"ViewerProtocolPolicy": "redirect-to-https",
"Compress": true
},
"DefaultRootObject": "index.html",
"Enabled": true,
"HttpVersion": "http2",
"IPV6Enabled": true,
"Origins": [
{
"DomainName": {
"Fn::GetAtt": [
"Bucket83908E77",
"RegionalDomainName"
]
},
"Id": "origin1",
"S3OriginConfig": {}
}
],
"PriceClass": "PriceClass_100",
"ViewerCertificate": {
"CloudFrontDefaultCertificate": true
},
"Restrictions": {
"GeoRestriction": {
"Locations": ["US", "UK"],
"RestrictionType": "whitelist"
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as s3 from '@aws-cdk/aws-s3';
import * as cdk from '@aws-cdk/core';
import * as cloudfront from '../lib';

const app = new cdk.App();

const stack = new cdk.Stack(app, 'cloudfront-geo-restrictions');

const sourceBucket = new s3.Bucket(stack, 'Bucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});

new cloudfront.CloudFrontWebDistribution(stack, 'MyDistribution', {
originConfigs: [
{
s3OriginSource: {
s3BucketSource: sourceBucket,
},
behaviors : [ {isDefaultBehavior: true}],
},
],
geoRestriction: cloudfront.GeoRestriction.whitelist('US', 'UK'),
});

app.synth();
187 changes: 185 additions & 2 deletions packages/@aws-cdk/aws-cloudfront/test/test.basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ import * as s3 from '@aws-cdk/aws-s3';
import * as cdk from '@aws-cdk/core';
import { Test } from 'nodeunit';
import {
CfnDistribution, CloudFrontWebDistribution, LambdaEdgeEventType, SecurityPolicyProtocol, SSLMethod,
ViewerCertificate, ViewerProtocolPolicy,
CfnDistribution,
CloudFrontWebDistribution,
GeoRestriction,
LambdaEdgeEventType,
SecurityPolicyProtocol,
SSLMethod,
ViewerCertificate,
ViewerProtocolPolicy,
} from '../lib';

// tslint:disable:object-literal-key-quotes
Expand Down Expand Up @@ -870,4 +876,181 @@ export = {
expect(stack).notTo(haveResourceLike('AWS::IAM::Role'));
test.done();
},

'geo restriction': {
'success' : {
'whitelist'(test: Test) {
const stack = new cdk.Stack();
const sourceBucket = new s3.Bucket(stack, 'Bucket');

new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', {
originConfigs: [{
s3OriginSource: {s3BucketSource: sourceBucket},
behaviors: [{isDefaultBehavior: true}],
}],
geoRestriction: GeoRestriction.whitelist('US', 'UK'),
});

expect(stack).toMatch({
'Resources': {
'Bucket83908E77': {
'Type': 'AWS::S3::Bucket',
'DeletionPolicy': 'Retain',
'UpdateReplacePolicy': 'Retain',
},
'AnAmazingWebsiteProbablyCFDistribution47E3983B': {
'Type': 'AWS::CloudFront::Distribution',
'Properties': {
'DistributionConfig': {
'DefaultRootObject': 'index.html',
'Origins': [
{
'DomainName': {
'Fn::GetAtt': [
'Bucket83908E77',
'RegionalDomainName',
],
},
'Id': 'origin1',
'S3OriginConfig': {},
},
],
'ViewerCertificate': {
'CloudFrontDefaultCertificate': true,
},
'PriceClass': 'PriceClass_100',
'DefaultCacheBehavior': {
'AllowedMethods': [
'GET',
'HEAD',
],
'CachedMethods': [
'GET',
'HEAD',
],
'TargetOriginId': 'origin1',
'ViewerProtocolPolicy': 'redirect-to-https',
'ForwardedValues': {
'QueryString': false,
'Cookies': {'Forward': 'none'},
},
'Compress': true,
},
'Enabled': true,
'IPV6Enabled': true,
'HttpVersion': 'http2',
'Restrictions': {
'GeoRestriction': {
'Locations': ['US', 'UK'],
'RestrictionType': 'whitelist',
},
},
},
},
},
},
});

test.done();
},
'blacklist'(test: Test) {
const stack = new cdk.Stack();
const sourceBucket = new s3.Bucket(stack, 'Bucket');

new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', {
originConfigs: [{
s3OriginSource: {s3BucketSource: sourceBucket},
behaviors: [{isDefaultBehavior: true}],
}],
geoRestriction: GeoRestriction.blacklist('US'),
});

expect(stack).toMatch({
'Resources': {
'Bucket83908E77': {
'Type': 'AWS::S3::Bucket',
'DeletionPolicy': 'Retain',
'UpdateReplacePolicy': 'Retain',
},
'AnAmazingWebsiteProbablyCFDistribution47E3983B': {
'Type': 'AWS::CloudFront::Distribution',
'Properties': {
'DistributionConfig': {
'DefaultRootObject': 'index.html',
'Origins': [
{
'DomainName': {
'Fn::GetAtt': [
'Bucket83908E77',
'RegionalDomainName',
],
},
'Id': 'origin1',
'S3OriginConfig': {},
},
],
'ViewerCertificate': {
'CloudFrontDefaultCertificate': true,
},
'PriceClass': 'PriceClass_100',
'DefaultCacheBehavior': {
'AllowedMethods': [
'GET',
'HEAD',
],
'CachedMethods': [
'GET',
'HEAD',
],
'TargetOriginId': 'origin1',
'ViewerProtocolPolicy': 'redirect-to-https',
'ForwardedValues': {
'QueryString': false,
'Cookies': {'Forward': 'none'},
},
'Compress': true,
},
'Enabled': true,
'IPV6Enabled': true,
'HttpVersion': 'http2',
'Restrictions': {
'GeoRestriction': {
'Locations': ['US'],
'RestrictionType': 'blacklist',
},
},
},
},
},
},
});

test.done();
},
},
'error': {
'throws if locations is empty array'(test: Test) {
test.throws(() => {
GeoRestriction.whitelist();
}, 'Should provide at least 1 location');

test.throws(() => {
GeoRestriction.blacklist();
}, 'Should provide at least 1 location');

test.done();
},
'throws if locations format is wrong'(test: Test) {
test.throws(() => {
GeoRestriction.whitelist('us');
}, 'Invalid location format for location: us, location should be two-letter and uppercase country ISO 3166-1-alpha-2 code');

test.throws(() => {
GeoRestriction.blacklist('us');
}, 'Invalid location format for location: us, location should be two-letter and uppercase country ISO 3166-1-alpha-2 code');

test.done();
},
},
},
};

0 comments on commit cf25ba0

Please sign in to comment.