@digitraffic/common 2024.5.13-1 → 2024.6.14-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
|
|
1
1
|
import { AclBuilder } from "../../aws/infra/acl-builder.mjs";
|
2
2
|
import { App, Stack } from "aws-cdk-lib";
|
3
|
+
import { expect } from "@jest/globals";
|
3
4
|
describe("acl-builder tests", () => {
|
4
5
|
function createBuilder() {
|
5
6
|
const app = new App();
|
@@ -21,5 +22,28 @@ describe("acl-builder tests", () => {
|
|
21
22
|
const acl = createBuilder().withIpRestrictionRule(["1.2.3.4", "1.2.6.6"]).build();
|
22
23
|
expect(acl.rules).toHaveLength(1);
|
23
24
|
});
|
25
|
+
test("throttle rules", () => {
|
26
|
+
for (const aclBuilder of [
|
27
|
+
createBuilder().withThrottleDigitrafficUserIp(100),
|
28
|
+
createBuilder().withThrottleDigitrafficUserIpAndUriPath(100),
|
29
|
+
createBuilder().withThrottleAnonymousUserIp(100),
|
30
|
+
createBuilder().withThrottleAnonymousUserIpAndUriPath(100)
|
31
|
+
]) {
|
32
|
+
const acl = aclBuilder.build();
|
33
|
+
// Check that the rule exists and a custom response is defined
|
34
|
+
expect(acl.rules).toHaveLength(1);
|
35
|
+
expect(Object.keys(acl.customResponseBodies)).toHaveLength(1);
|
36
|
+
// Check that the rule does throttle
|
37
|
+
const throttleRule = acl.rules[0];
|
38
|
+
expect(throttleRule.statement.rateBasedStatement).toBeDefined();
|
39
|
+
expect(throttleRule.action.block).toBeDefined();
|
40
|
+
}
|
41
|
+
});
|
42
|
+
test("Cannot define two rules with the same name", () => {
|
43
|
+
expect(() => createBuilder()
|
44
|
+
.withThrottleAnonymousUserIp(10)
|
45
|
+
.withThrottleAnonymousUserIp(200)
|
46
|
+
.build()).toThrow();
|
47
|
+
});
|
24
48
|
});
|
25
49
|
//# sourceMappingURL=acl-builder.test.mjs.map
|
@@ -13,9 +13,19 @@ export declare class AclBuilder {
|
|
13
13
|
readonly _rules: CfnWebACL.RuleProperty[];
|
14
14
|
_scope: string;
|
15
15
|
_name: string;
|
16
|
+
_customResponseBodies: Record<string, CfnWebACL.CustomResponseBodyProperty>;
|
16
17
|
constructor(construct: Construct);
|
17
18
|
isRuleDefined(rules: AWSManagedWafRule[] | "all", rule: AWSManagedWafRule): boolean;
|
18
19
|
withAWSManagedRules(rules?: AWSManagedWafRule[] | "all"): AclBuilder;
|
19
20
|
withIpRestrictionRule(addresses: string[]): AclBuilder;
|
21
|
+
withThrottleRule(name: string, priority: number, limit: number, customResponseBodyKey: string, isHeaderRequired: boolean, isBasedOnIpAndUriPath: boolean): AclBuilder;
|
22
|
+
withCustomResponseBody(key: string, customResponseBody: CfnWebACL.CustomResponseBodyProperty): this;
|
23
|
+
withThrottleDigitrafficUserIp(limit: number): AclBuilder;
|
24
|
+
withThrottleDigitrafficUserIpAndUriPath(limit: number): AclBuilder;
|
25
|
+
withThrottleAnonymousUserIp(limit: number): AclBuilder;
|
26
|
+
withThrottleAnonymousUserIpAndUriPath(limit: number): AclBuilder;
|
27
|
+
_isCustomResponseBodyKeySet(key: string): boolean;
|
28
|
+
_addThrottleResponseBody(customResponseBodyKey: string, limit: number): void;
|
29
|
+
_logMissingLimit(method: string): void;
|
20
30
|
build(): CfnWebACL;
|
21
31
|
}
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { CfnIPSet, CfnWebACL } from "aws-cdk-lib/aws-wafv2";
|
2
|
+
import { logger } from "../runtime/dt-logger-default.mjs";
|
2
3
|
/**
|
3
4
|
* Builder class for building CfnWebACL.
|
4
5
|
*
|
@@ -11,6 +12,7 @@ export class AclBuilder {
|
|
11
12
|
_rules = [];
|
12
13
|
_scope = "CLOUDFRONT";
|
13
14
|
_name = "WebACL";
|
15
|
+
_customResponseBodies = {};
|
14
16
|
constructor(construct) {
|
15
17
|
this._construct = construct;
|
16
18
|
}
|
@@ -55,10 +57,98 @@ export class AclBuilder {
|
|
55
57
|
});
|
56
58
|
return this;
|
57
59
|
}
|
60
|
+
withThrottleRule(name, priority, limit, customResponseBodyKey, isHeaderRequired, isBasedOnIpAndUriPath) {
|
61
|
+
this._rules.push({
|
62
|
+
name,
|
63
|
+
priority,
|
64
|
+
visibilityConfig: {
|
65
|
+
sampledRequestsEnabled: true,
|
66
|
+
cloudWatchMetricsEnabled: true,
|
67
|
+
metricName: name
|
68
|
+
},
|
69
|
+
action: {
|
70
|
+
block: {
|
71
|
+
customResponse: {
|
72
|
+
responseCode: 429,
|
73
|
+
customResponseBodyKey
|
74
|
+
}
|
75
|
+
}
|
76
|
+
},
|
77
|
+
statement: createThrottleStatement(limit, isHeaderRequired, isBasedOnIpAndUriPath)
|
78
|
+
});
|
79
|
+
return this;
|
80
|
+
}
|
81
|
+
withCustomResponseBody(key, customResponseBody) {
|
82
|
+
if (key in this._customResponseBodies) {
|
83
|
+
logger.warn({
|
84
|
+
method: "acl-builder.withCustomResponseBody",
|
85
|
+
message: `Overriding custom response body with key ${key}`
|
86
|
+
});
|
87
|
+
}
|
88
|
+
this._customResponseBodies[key] = customResponseBody;
|
89
|
+
return this;
|
90
|
+
}
|
91
|
+
withThrottleDigitrafficUserIp(limit) {
|
92
|
+
if (limit == null) {
|
93
|
+
this._logMissingLimit("withThrottleDigitrafficUserIp");
|
94
|
+
return this;
|
95
|
+
}
|
96
|
+
const customResponseBodyKey = `IP_THROTTLE_DIGITRAFFIC_USER_${limit}`;
|
97
|
+
this._addThrottleResponseBody(customResponseBodyKey, limit);
|
98
|
+
return this.withThrottleRule("ThrottleRuleWithDigitrafficUser", 1, limit, customResponseBodyKey, true, false);
|
99
|
+
}
|
100
|
+
withThrottleDigitrafficUserIpAndUriPath(limit) {
|
101
|
+
if (limit == null) {
|
102
|
+
this._logMissingLimit("withThrottleDigitrafficUserIpAndUriPath");
|
103
|
+
return this;
|
104
|
+
}
|
105
|
+
const customResponseBodyKey = `IP_PATH_THROTTLE_DIGITRAFFIC_USER_${limit}`;
|
106
|
+
this._addThrottleResponseBody(customResponseBodyKey, limit);
|
107
|
+
return this.withThrottleRule("ThrottleRuleIPQueryWithDigitrafficUser", 2, limit, customResponseBodyKey, true, true);
|
108
|
+
}
|
109
|
+
withThrottleAnonymousUserIp(limit) {
|
110
|
+
if (limit == null) {
|
111
|
+
this._logMissingLimit("withThrottleAnonymousUserIp");
|
112
|
+
return this;
|
113
|
+
}
|
114
|
+
const customResponseBodyKey = `IP_THROTTLE_ANONYMOUS_USER_${limit}`;
|
115
|
+
this._addThrottleResponseBody(customResponseBodyKey, limit);
|
116
|
+
return this.withThrottleRule("ThrottleRuleWithAnonymousUser", 3, limit, customResponseBodyKey, false, false);
|
117
|
+
}
|
118
|
+
withThrottleAnonymousUserIpAndUriPath(limit) {
|
119
|
+
if (limit == null) {
|
120
|
+
this._logMissingLimit("withThrottleAnonymousUserIpAndUriPath");
|
121
|
+
return this;
|
122
|
+
}
|
123
|
+
const customResponseBodyKey = `IP_PATH_THROTTLE_ANONYMOUS_USER_${limit}`;
|
124
|
+
this._addThrottleResponseBody(customResponseBodyKey, limit);
|
125
|
+
return this.withThrottleRule("ThrottleRuleIPQueryWithAnonymousUser", 4, limit, customResponseBodyKey, false, true);
|
126
|
+
}
|
127
|
+
_isCustomResponseBodyKeySet(key) {
|
128
|
+
return key in this._customResponseBodies;
|
129
|
+
}
|
130
|
+
_addThrottleResponseBody(customResponseBodyKey, limit) {
|
131
|
+
if (!this._isCustomResponseBodyKeySet(customResponseBodyKey)) {
|
132
|
+
this.withCustomResponseBody(customResponseBodyKey, {
|
133
|
+
content: `Request rate is limited to ${limit} requests in a 5 minute window.`,
|
134
|
+
contentType: "TEXT_PLAIN"
|
135
|
+
});
|
136
|
+
}
|
137
|
+
}
|
138
|
+
_logMissingLimit(method) {
|
139
|
+
logger.warn({
|
140
|
+
method: `acl-builder.${method}`,
|
141
|
+
message: `'limit' was not defined. Not setting a throttle rule`
|
142
|
+
});
|
143
|
+
}
|
58
144
|
build() {
|
59
145
|
if (this._rules.length === 0) {
|
60
146
|
throw new Error("No rules defined for WebACL");
|
61
147
|
}
|
148
|
+
const uniqueRuleNames = new Set(this._rules.map(rule => rule.name));
|
149
|
+
if (uniqueRuleNames.size != this._rules.length) {
|
150
|
+
throw new Error("Tried to create an Access Control List with multiple rules having the same name");
|
151
|
+
}
|
62
152
|
const acl = new CfnWebACL(this._construct, this._name, {
|
63
153
|
defaultAction: { allow: {} },
|
64
154
|
scope: this._scope,
|
@@ -68,11 +158,75 @@ export class AclBuilder {
|
|
68
158
|
sampledRequestsEnabled: false
|
69
159
|
},
|
70
160
|
rules: this._rules,
|
71
|
-
|
161
|
+
customResponseBodies: this._customResponseBodies,
|
72
162
|
});
|
73
163
|
return acl;
|
74
164
|
}
|
75
165
|
}
|
166
|
+
const CUSTOM_KEYS_IP_AND_URI_PATH = [
|
167
|
+
{
|
168
|
+
uriPath: {
|
169
|
+
textTransformations: [
|
170
|
+
{
|
171
|
+
priority: 1,
|
172
|
+
type: "LOWERCASE"
|
173
|
+
},
|
174
|
+
{
|
175
|
+
priority: 2,
|
176
|
+
type: "NORMALIZE_PATH"
|
177
|
+
},
|
178
|
+
{
|
179
|
+
priority: 3,
|
180
|
+
type: "MD5"
|
181
|
+
}
|
182
|
+
]
|
183
|
+
}
|
184
|
+
},
|
185
|
+
{
|
186
|
+
ip: {}
|
187
|
+
}
|
188
|
+
];
|
189
|
+
function notStatement(statement) {
|
190
|
+
return {
|
191
|
+
notStatement: {
|
192
|
+
statement
|
193
|
+
}
|
194
|
+
};
|
195
|
+
}
|
196
|
+
function createThrottleStatement(limit, isHeaderRequired, isBasedOnIpAndUriPath) {
|
197
|
+
// this statement matches empty digitraffic-user -header
|
198
|
+
const matchStatement = {
|
199
|
+
sizeConstraintStatement: {
|
200
|
+
comparisonOperator: isHeaderRequired ? "GT" : "GE",
|
201
|
+
fieldToMatch: {
|
202
|
+
singleHeader: {
|
203
|
+
Name: "digitraffic-user"
|
204
|
+
}
|
205
|
+
},
|
206
|
+
textTransformations: [{ priority: 0, type: "NONE" }],
|
207
|
+
size: 0
|
208
|
+
}
|
209
|
+
};
|
210
|
+
// header present -> size > 0
|
211
|
+
// header not present -> NOT(size >= 0)
|
212
|
+
if (isBasedOnIpAndUriPath) {
|
213
|
+
return {
|
214
|
+
rateBasedStatement: {
|
215
|
+
aggregateKeyType: "CUSTOM_KEYS",
|
216
|
+
customKeys: CUSTOM_KEYS_IP_AND_URI_PATH,
|
217
|
+
limit: limit,
|
218
|
+
scopeDownStatement: isHeaderRequired ? matchStatement : notStatement(matchStatement)
|
219
|
+
}
|
220
|
+
};
|
221
|
+
}
|
222
|
+
return {
|
223
|
+
rateBasedStatement: {
|
224
|
+
aggregateKeyType: "IP",
|
225
|
+
limit: limit,
|
226
|
+
scopeDownStatement: isHeaderRequired ? matchStatement : notStatement(matchStatement)
|
227
|
+
}
|
228
|
+
};
|
229
|
+
}
|
76
230
|
function createAWSCommonRuleSet() {
|
77
231
|
return createRuleProperty("AWS-AWSManagedRulesCommonRuleSet", 70, {
|
78
232
|
statement: {
|