Skip to content

Commit cf25ba0

Browse files
authored
feat(cloudfront): support geo restrictions for cloudfront distribution (#7345)
- Restrictions currently support GeoRestriction only, but if CloudFront adds support for other types it can easily be expanded - Closes #3456
1 parent 87860ca commit cf25ba0

File tree

5 files changed

+356
-2
lines changed

5 files changed

+356
-2
lines changed

packages/@aws-cdk/aws-cloudfront/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,17 @@ See [Importing an SSL/TLS Certificate](https://docs.aws.amazon.com/AmazonCloudFr
6767
Example:
6868

6969
[create a distrubution with an iam certificate example](test/example.iam-cert-alias.lit.ts)
70+
71+
#### Restrictions
72+
73+
CloudFront supports adding restrictions to your distribution.
74+
75+
See [Restricting the Geographic Distribution of Your Content](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/georestrictions.html) in the CloudFront User Guide.
76+
77+
Example:
78+
```ts
79+
new cloudfront.CloudFrontWebDistribution(stack, 'MyDistribution', {
80+
//...
81+
geoRestriction: GeoRestriction.whitelist('US', 'UK')
82+
});
83+
```

packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,58 @@ export class ViewerCertificate {
482482
public readonly aliases: string[] = []) { }
483483
}
484484

485+
/**
486+
* Controls the countries in which your content is distributed.
487+
*/
488+
export class GeoRestriction {
489+
490+
/**
491+
* Whitelist specific countries which you want CloudFront to distribute your content.
492+
*
493+
* @param locations Two-letter, uppercase country code for a country
494+
* that you want to whitelist. Include one element for each country.
495+
* See ISO 3166-1-alpha-2 code on the *International Organization for Standardization* website
496+
*/
497+
public static whitelist(...locations: string[]) {
498+
return new GeoRestriction('whitelist', GeoRestriction.validateLocations(locations));
499+
}
500+
501+
/**
502+
* Blacklist specific countries which you don't want CloudFront to distribute your content.
503+
*
504+
* @param locations Two-letter, uppercase country code for a country
505+
* that you want to blacklist. Include one element for each country.
506+
* See ISO 3166-1-alpha-2 code on the *International Organization for Standardization* website
507+
*/
508+
public static blacklist(...locations: string[]) {
509+
return new GeoRestriction('blacklist', GeoRestriction.validateLocations(locations));
510+
}
511+
512+
private static LOCATION_REGEX = /^[A-Z]{2}$/;
513+
514+
private static validateLocations(locations: string[]) {
515+
if (locations.length === 0) {
516+
throw new Error('Should provide at least 1 location');
517+
}
518+
locations.forEach(location => {
519+
if (!GeoRestriction.LOCATION_REGEX.test(location)) {
520+
throw new Error(`Invalid location format for location: ${location}, location should be two-letter and uppercase country ISO 3166-1-alpha-2 code`);
521+
}
522+
});
523+
return locations;
524+
}
525+
526+
/**
527+
* Creates an instance of GeoRestriction for internal use
528+
*
529+
* @param restrictionType Specifies the restriction type to impose (whitelist or blacklist)
530+
* @param locations Two-letter, uppercase country code for a country
531+
* that you want to whitelist/blacklist. Include one element for each country.
532+
* See ISO 3166-1-alpha-2 code on the *International Organization for Standardization* website
533+
*/
534+
private constructor(readonly restrictionType: 'whitelist' | 'blacklist', readonly locations: string[]) {}
535+
}
536+
485537
export interface CloudFrontWebDistributionProps {
486538

487539
/**
@@ -576,6 +628,13 @@ export interface CloudFrontWebDistributionProps {
576628
* @see https://aws.amazon.com/premiumsupport/knowledge-center/custom-ssl-certificate-cloudfront/
577629
*/
578630
readonly viewerCertificate?: ViewerCertificate;
631+
632+
/**
633+
* Controls the countries in which your content is distributed.
634+
*
635+
* @default No geo restriction
636+
*/
637+
readonly geoRestriction?: GeoRestriction;
579638
}
580639

581640
/**
@@ -818,6 +877,18 @@ export class CloudFrontWebDistribution extends cdk.Construct implements IDistrib
818877
};
819878
}
820879

880+
if (props.geoRestriction) {
881+
distributionConfig = {
882+
...distributionConfig,
883+
restrictions: {
884+
geoRestriction: {
885+
restrictionType: props.geoRestriction.restrictionType,
886+
locations: props.geoRestriction.locations,
887+
},
888+
},
889+
};
890+
}
891+
821892
const distribution = new CfnDistribution(this, 'CFDistribution', { distributionConfig });
822893
this.node.defaultChild = distribution;
823894
this.domainName = distribution.attrDomainName;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"Resources": {
3+
"Bucket83908E77": {
4+
"DeletionPolicy": "Delete",
5+
"UpdateReplacePolicy": "Delete",
6+
"Type": "AWS::S3::Bucket"
7+
},
8+
"MyDistributionCFDistributionDE147309": {
9+
"Type": "AWS::CloudFront::Distribution",
10+
"Properties": {
11+
"DistributionConfig": {
12+
"DefaultCacheBehavior": {
13+
"AllowedMethods": [
14+
"GET",
15+
"HEAD"
16+
],
17+
"CachedMethods": [
18+
"GET",
19+
"HEAD"
20+
],
21+
"ForwardedValues": {
22+
"Cookies": {
23+
"Forward": "none"
24+
},
25+
"QueryString": false
26+
},
27+
"TargetOriginId": "origin1",
28+
"ViewerProtocolPolicy": "redirect-to-https",
29+
"Compress": true
30+
},
31+
"DefaultRootObject": "index.html",
32+
"Enabled": true,
33+
"HttpVersion": "http2",
34+
"IPV6Enabled": true,
35+
"Origins": [
36+
{
37+
"DomainName": {
38+
"Fn::GetAtt": [
39+
"Bucket83908E77",
40+
"RegionalDomainName"
41+
]
42+
},
43+
"Id": "origin1",
44+
"S3OriginConfig": {}
45+
}
46+
],
47+
"PriceClass": "PriceClass_100",
48+
"ViewerCertificate": {
49+
"CloudFrontDefaultCertificate": true
50+
},
51+
"Restrictions": {
52+
"GeoRestriction": {
53+
"Locations": ["US", "UK"],
54+
"RestrictionType": "whitelist"
55+
}
56+
}
57+
}
58+
}
59+
}
60+
}
61+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as s3 from '@aws-cdk/aws-s3';
2+
import * as cdk from '@aws-cdk/core';
3+
import * as cloudfront from '../lib';
4+
5+
const app = new cdk.App();
6+
7+
const stack = new cdk.Stack(app, 'cloudfront-geo-restrictions');
8+
9+
const sourceBucket = new s3.Bucket(stack, 'Bucket', {
10+
removalPolicy: cdk.RemovalPolicy.DESTROY,
11+
});
12+
13+
new cloudfront.CloudFrontWebDistribution(stack, 'MyDistribution', {
14+
originConfigs: [
15+
{
16+
s3OriginSource: {
17+
s3BucketSource: sourceBucket,
18+
},
19+
behaviors : [ {isDefaultBehavior: true}],
20+
},
21+
],
22+
geoRestriction: cloudfront.GeoRestriction.whitelist('US', 'UK'),
23+
});
24+
25+
app.synth();

packages/@aws-cdk/aws-cloudfront/test/test.basic.ts

Lines changed: 185 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ import * as s3 from '@aws-cdk/aws-s3';
55
import * as cdk from '@aws-cdk/core';
66
import { Test } from 'nodeunit';
77
import {
8-
CfnDistribution, CloudFrontWebDistribution, LambdaEdgeEventType, SecurityPolicyProtocol, SSLMethod,
9-
ViewerCertificate, ViewerProtocolPolicy,
8+
CfnDistribution,
9+
CloudFrontWebDistribution,
10+
GeoRestriction,
11+
LambdaEdgeEventType,
12+
SecurityPolicyProtocol,
13+
SSLMethod,
14+
ViewerCertificate,
15+
ViewerProtocolPolicy,
1016
} from '../lib';
1117

1218
// tslint:disable:object-literal-key-quotes
@@ -870,4 +876,181 @@ export = {
870876
expect(stack).notTo(haveResourceLike('AWS::IAM::Role'));
871877
test.done();
872878
},
879+
880+
'geo restriction': {
881+
'success' : {
882+
'whitelist'(test: Test) {
883+
const stack = new cdk.Stack();
884+
const sourceBucket = new s3.Bucket(stack, 'Bucket');
885+
886+
new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', {
887+
originConfigs: [{
888+
s3OriginSource: {s3BucketSource: sourceBucket},
889+
behaviors: [{isDefaultBehavior: true}],
890+
}],
891+
geoRestriction: GeoRestriction.whitelist('US', 'UK'),
892+
});
893+
894+
expect(stack).toMatch({
895+
'Resources': {
896+
'Bucket83908E77': {
897+
'Type': 'AWS::S3::Bucket',
898+
'DeletionPolicy': 'Retain',
899+
'UpdateReplacePolicy': 'Retain',
900+
},
901+
'AnAmazingWebsiteProbablyCFDistribution47E3983B': {
902+
'Type': 'AWS::CloudFront::Distribution',
903+
'Properties': {
904+
'DistributionConfig': {
905+
'DefaultRootObject': 'index.html',
906+
'Origins': [
907+
{
908+
'DomainName': {
909+
'Fn::GetAtt': [
910+
'Bucket83908E77',
911+
'RegionalDomainName',
912+
],
913+
},
914+
'Id': 'origin1',
915+
'S3OriginConfig': {},
916+
},
917+
],
918+
'ViewerCertificate': {
919+
'CloudFrontDefaultCertificate': true,
920+
},
921+
'PriceClass': 'PriceClass_100',
922+
'DefaultCacheBehavior': {
923+
'AllowedMethods': [
924+
'GET',
925+
'HEAD',
926+
],
927+
'CachedMethods': [
928+
'GET',
929+
'HEAD',
930+
],
931+
'TargetOriginId': 'origin1',
932+
'ViewerProtocolPolicy': 'redirect-to-https',
933+
'ForwardedValues': {
934+
'QueryString': false,
935+
'Cookies': {'Forward': 'none'},
936+
},
937+
'Compress': true,
938+
},
939+
'Enabled': true,
940+
'IPV6Enabled': true,
941+
'HttpVersion': 'http2',
942+
'Restrictions': {
943+
'GeoRestriction': {
944+
'Locations': ['US', 'UK'],
945+
'RestrictionType': 'whitelist',
946+
},
947+
},
948+
},
949+
},
950+
},
951+
},
952+
});
953+
954+
test.done();
955+
},
956+
'blacklist'(test: Test) {
957+
const stack = new cdk.Stack();
958+
const sourceBucket = new s3.Bucket(stack, 'Bucket');
959+
960+
new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', {
961+
originConfigs: [{
962+
s3OriginSource: {s3BucketSource: sourceBucket},
963+
behaviors: [{isDefaultBehavior: true}],
964+
}],
965+
geoRestriction: GeoRestriction.blacklist('US'),
966+
});
967+
968+
expect(stack).toMatch({
969+
'Resources': {
970+
'Bucket83908E77': {
971+
'Type': 'AWS::S3::Bucket',
972+
'DeletionPolicy': 'Retain',
973+
'UpdateReplacePolicy': 'Retain',
974+
},
975+
'AnAmazingWebsiteProbablyCFDistribution47E3983B': {
976+
'Type': 'AWS::CloudFront::Distribution',
977+
'Properties': {
978+
'DistributionConfig': {
979+
'DefaultRootObject': 'index.html',
980+
'Origins': [
981+
{
982+
'DomainName': {
983+
'Fn::GetAtt': [
984+
'Bucket83908E77',
985+
'RegionalDomainName',
986+
],
987+
},
988+
'Id': 'origin1',
989+
'S3OriginConfig': {},
990+
},
991+
],
992+
'ViewerCertificate': {
993+
'CloudFrontDefaultCertificate': true,
994+
},
995+
'PriceClass': 'PriceClass_100',
996+
'DefaultCacheBehavior': {
997+
'AllowedMethods': [
998+
'GET',
999+
'HEAD',
1000+
],
1001+
'CachedMethods': [
1002+
'GET',
1003+
'HEAD',
1004+
],
1005+
'TargetOriginId': 'origin1',
1006+
'ViewerProtocolPolicy': 'redirect-to-https',
1007+
'ForwardedValues': {
1008+
'QueryString': false,
1009+
'Cookies': {'Forward': 'none'},
1010+
},
1011+
'Compress': true,
1012+
},
1013+
'Enabled': true,
1014+
'IPV6Enabled': true,
1015+
'HttpVersion': 'http2',
1016+
'Restrictions': {
1017+
'GeoRestriction': {
1018+
'Locations': ['US'],
1019+
'RestrictionType': 'blacklist',
1020+
},
1021+
},
1022+
},
1023+
},
1024+
},
1025+
},
1026+
});
1027+
1028+
test.done();
1029+
},
1030+
},
1031+
'error': {
1032+
'throws if locations is empty array'(test: Test) {
1033+
test.throws(() => {
1034+
GeoRestriction.whitelist();
1035+
}, 'Should provide at least 1 location');
1036+
1037+
test.throws(() => {
1038+
GeoRestriction.blacklist();
1039+
}, 'Should provide at least 1 location');
1040+
1041+
test.done();
1042+
},
1043+
'throws if locations format is wrong'(test: Test) {
1044+
test.throws(() => {
1045+
GeoRestriction.whitelist('us');
1046+
}, 'Invalid location format for location: us, location should be two-letter and uppercase country ISO 3166-1-alpha-2 code');
1047+
1048+
test.throws(() => {
1049+
GeoRestriction.blacklist('us');
1050+
}, 'Invalid location format for location: us, location should be two-letter and uppercase country ISO 3166-1-alpha-2 code');
1051+
1052+
test.done();
1053+
},
1054+
},
1055+
},
8731056
};

0 commit comments

Comments
 (0)