Skip to content

Commit de12ed3

Browse files
authored
Merge pull request #6293 from axel-springer-kugawana/add-alb-event-conditions
Add ip, method, header and query conditions to ALB events
2 parents 40c5383 + 6eeba11 commit de12ed3

File tree

5 files changed

+270
-11
lines changed

5 files changed

+270
-11
lines changed

docs/providers/aws/events/alb.md

+29
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ The Serverless Framework makes it possible to setup the connection between Appli
1818

1919
## Event definition
2020

21+
```yml
22+
functions:
23+
albEventConsumer:
24+
handler: handler.hello
25+
events:
26+
- alb:
27+
listenerArn: arn:aws:elasticloadbalancing:us-east-1:12345:listener/app/my-load-balancer/50dc6c495c0c9188/
28+
priority: 1
29+
conditions:
30+
path: /hello
31+
```
32+
33+
## Using different conditions
34+
2135
```yml
2236
functions:
2337
albEventConsumer:
@@ -29,4 +43,19 @@ functions:
2943
conditions:
3044
host: example.com
3145
path: /hello
46+
method:
47+
- POST
48+
- PATCH
49+
host:
50+
- example.com
51+
- example2.com
52+
header:
53+
name: foo
54+
values:
55+
- bar
56+
query:
57+
bar: true
58+
ip:
59+
- fe80:0000:0000:0000:0204:61ff:fe9d:f156/6
60+
- 192.168.0.1/0
3261
```

lib/plugins/aws/package/compile/events/alb/lib/listenerRules.js

+38-3
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,51 @@ module.exports = {
1111
const Conditions = [
1212
{
1313
Field: 'path-pattern',
14-
Values: [event.conditions.path],
14+
Values: event.conditions.path,
1515
},
1616
];
1717
if (event.conditions.host) {
1818
Conditions.push({
1919
Field: 'host-header',
20-
Values: [event.conditions.host],
20+
Values: event.conditions.host,
21+
});
22+
}
23+
if (event.conditions.method) {
24+
Conditions.push({
25+
Field: 'http-request-method',
26+
HttpRequestMethodConfig: {
27+
Values: event.conditions.method,
28+
},
29+
});
30+
}
31+
if (event.conditions.header) {
32+
Conditions.push({
33+
Field: 'http-header',
34+
HttpHeaderConfig: {
35+
HttpHeaderName: event.conditions.header.name,
36+
Values: event.conditions.header.values,
37+
},
38+
});
39+
}
40+
if (event.conditions.query) {
41+
Conditions.push({
42+
Field: 'query-string',
43+
QueryStringConfig: {
44+
Values: Object.keys(event.conditions.query).map(key => ({
45+
Key: key,
46+
Value: event.conditions.query[key],
47+
})),
48+
},
49+
});
50+
}
51+
if (event.conditions.ip) {
52+
Conditions.push({
53+
Field: 'source-ip',
54+
SourceIpConfig: {
55+
Values: event.conditions.ip,
56+
},
2157
});
2258
}
23-
2459
Object.assign(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
2560
[listenerRuleLogicalId]: {
2661
Type: 'AWS::ElasticLoadBalancingV2::ListenerRule',

lib/plugins/aws/package/compile/events/alb/lib/listenerRules.test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ describe('#compileListenerRules()', () => {
2727
+ '50dc6c495c0c9188/f2f7dc8efc522ab2',
2828
priority: 1,
2929
conditions: {
30-
host: 'example.com',
31-
path: '/hello',
30+
host: ['example.com'],
31+
path: ['/hello'],
3232
},
3333
},
3434
{
@@ -38,7 +38,7 @@ describe('#compileListenerRules()', () => {
3838
+ '50dc6c495c0c9188/f2f7dc8efc522ab2',
3939
priority: 2,
4040
conditions: {
41-
path: '/world',
41+
path: ['/world'],
4242
},
4343
},
4444
],

lib/plugins/aws/package/compile/events/alb/lib/validate.js

+66-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
const _ = require('lodash');
44

5+
// eslint-disable-next-line max-len
6+
const CIDR_IPV6_PATTERN = /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))$/;
7+
const CIDR_IPV4_PATTERN = /^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))$/;
8+
59
module.exports = {
610
validate() {
711
const events = [];
@@ -14,13 +18,26 @@ module.exports = {
1418
listenerArn: event.alb.listenerArn,
1519
priority: event.alb.priority,
1620
conditions: {
17-
path: event.alb.conditions.path,
21+
// concat usage allows the user to provide value as a string or an array
22+
path: _.concat(event.alb.conditions.path),
1823
},
1924
// the following is data which is not defined on the event-level
2025
functionName,
2126
};
2227
if (event.alb.conditions.host) {
23-
albObj.conditions.host = event.alb.conditions.host;
28+
albObj.conditions.host = _.concat(event.alb.conditions.host);
29+
}
30+
if (event.alb.conditions.method) {
31+
albObj.conditions.method = _.concat(event.alb.conditions.method);
32+
}
33+
if (event.alb.conditions.header) {
34+
albObj.conditions.header = this.validateHeaderCondition(event, functionName);
35+
}
36+
if (event.alb.conditions.query) {
37+
albObj.conditions.query = this.validateQueryCondition(event, functionName);
38+
}
39+
if (event.alb.conditions.ip) {
40+
albObj.conditions.ip = this.validateIpCondition(event, functionName);
2441
}
2542
events.push(albObj);
2643
}
@@ -32,4 +49,51 @@ module.exports = {
3249
events,
3350
};
3451
},
52+
53+
validateHeaderCondition(event, functionName) {
54+
const messageTitle = `Invalid ALB event "header" condition in function "${functionName}".`;
55+
if (!_.isObject(event.alb.conditions.header)
56+
|| !event.alb.conditions.header.name
57+
|| !event.alb.conditions.header.values) {
58+
const errorMessage = [
59+
messageTitle,
60+
' You must provide an object with "name" and "values" properties.',
61+
].join('');
62+
throw new this.serverless.classes.Error(errorMessage);
63+
}
64+
if (!_.isArray(event.alb.conditions.header.values)) {
65+
const errorMessage = [
66+
messageTitle,
67+
' Property "values" must be an array.',
68+
].join('');
69+
throw new this.serverless.classes.Error(errorMessage);
70+
}
71+
return event.alb.conditions.header;
72+
},
73+
74+
validateQueryCondition(event, functionName) {
75+
if (!_.isObject(event.alb.conditions.query)) {
76+
const errorMessage = [
77+
`Invalid ALB event "query" condition in function "${functionName}".`,
78+
' You must provide an object.',
79+
].join('');
80+
throw new this.serverless.classes.Error(errorMessage);
81+
}
82+
return event.alb.conditions.query;
83+
},
84+
85+
validateIpCondition(event, functionName) {
86+
const cidrBlocks = _.concat(event.alb.conditions.ip);
87+
const allValuesAreCidr = cidrBlocks
88+
.every(cidr => CIDR_IPV4_PATTERN.test(cidr) || CIDR_IPV6_PATTERN.test(cidr));
89+
90+
if (!allValuesAreCidr) {
91+
const errorMessage = [
92+
`Invalid ALB event "ip" condition in function "${functionName}".`,
93+
' You must provide values in a valid IPv4 or IPv6 CIDR format.',
94+
].join('');
95+
throw new this.serverless.classes.Error(errorMessage);
96+
}
97+
return cidrBlocks;
98+
},
3599
};

lib/plugins/aws/package/compile/events/alb/lib/validate.test.js

+134-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ describe('#validate()', () => {
3030
conditions: {
3131
host: 'example.com',
3232
path: '/hello',
33+
method: 'GET',
34+
ip: ['192.168.0.1/1', 'fe80:0000:0000:0000:0204:61ff:fe9d:f156/3'],
3335
},
3436
},
3537
},
@@ -45,6 +47,10 @@ describe('#validate()', () => {
4547
priority: 2,
4648
conditions: {
4749
path: '/world',
50+
method: ['POST', 'GET'],
51+
query: {
52+
foo: 'bar',
53+
},
4854
},
4955
},
5056
},
@@ -62,8 +68,10 @@ describe('#validate()', () => {
6268
+ '50dc6c495c0c9188/f2f7dc8efc522ab2',
6369
priority: 1,
6470
conditions: {
65-
host: 'example.com',
66-
path: '/hello',
71+
host: ['example.com'],
72+
path: ['/hello'],
73+
method: ['GET'],
74+
ip: ['192.168.0.1/1', 'fe80:0000:0000:0000:0204:61ff:fe9d:f156/3'],
6775
},
6876
},
6977
{
@@ -73,9 +81,132 @@ describe('#validate()', () => {
7381
+ '50dc6c495c0c9188/f2f7dc8efc522ab2',
7482
priority: 2,
7583
conditions: {
76-
path: '/world',
84+
path: ['/world'],
85+
method: ['POST', 'GET'],
86+
query: {
87+
foo: 'bar',
88+
},
7789
},
7890
},
7991
]);
8092
});
93+
94+
it('should throw when given an invalid query condition', () => {
95+
awsCompileAlbEvents.serverless.service.functions = {
96+
first: {
97+
events: [
98+
{
99+
alb: {
100+
listenerArn: 'arn:aws:elasticloadbalancing:'
101+
+ 'us-east-1:123456789012:listener/app/my-load-balancer/'
102+
+ '50dc6c495c0c9188/f2f7dc8efc522ab2',
103+
priority: 1,
104+
conditions: {
105+
path: '/hello',
106+
query: 'ss',
107+
},
108+
},
109+
},
110+
],
111+
},
112+
};
113+
114+
expect(() => awsCompileAlbEvents.validate()).to.throw(Error);
115+
});
116+
117+
it('should throw when given an invalid ip condition', () => {
118+
awsCompileAlbEvents.serverless.service.functions = {
119+
first: {
120+
events: [
121+
{
122+
alb: {
123+
listenerArn: 'arn:aws:elasticloadbalancing:'
124+
+ 'us-east-1:123456789012:listener/app/my-load-balancer/'
125+
+ '50dc6c495c0c9188/f2f7dc8efc522ab2',
126+
priority: 1,
127+
conditions: {
128+
path: '/hello',
129+
ip: '1.1.1.1',
130+
},
131+
},
132+
},
133+
],
134+
},
135+
};
136+
137+
expect(() => awsCompileAlbEvents.validate()).to.throw(Error);
138+
});
139+
140+
it('should throw when given an invalid header condition', () => {
141+
awsCompileAlbEvents.serverless.service.functions = {
142+
first: {
143+
events: [
144+
{
145+
alb: {
146+
listenerArn: 'arn:aws:elasticloadbalancing:'
147+
+ 'us-east-1:123456789012:listener/app/my-load-balancer/'
148+
+ '50dc6c495c0c9188/f2f7dc8efc522ab2',
149+
priority: 1,
150+
conditions: {
151+
path: '/hello',
152+
header: ['foo'],
153+
},
154+
},
155+
},
156+
],
157+
},
158+
};
159+
160+
expect(() => awsCompileAlbEvents.validate()).to.throw(Error);
161+
});
162+
163+
describe('#validateIpCondition()', () => {
164+
it('should throw if ip is not a valid ipv6 or ipv4 cidr block', () => {
165+
const event = { alb: { conditions: { ip: 'fe80:0000:0000:0000:0204:61ff:fe9d:f156/' } } };
166+
expect(() => awsCompileAlbEvents.validateIpCondition(event, '')).to.throw(Error);
167+
});
168+
169+
it('should return the value as array if it is a valid ipv6 cidr block', () => {
170+
const event = { alb: { conditions: { ip: 'fe80:0000:0000:0000:0204:61ff:fe9d:f156/127' } } };
171+
expect(awsCompileAlbEvents.validateIpCondition(event, ''))
172+
.to.deep.equal(['fe80:0000:0000:0000:0204:61ff:fe9d:f156/127']);
173+
});
174+
175+
it('should return the value as array if it is a valid ipv4 cidr block', () => {
176+
const event = { alb: { conditions: { ip: '192.168.0.1/21' } } };
177+
expect(awsCompileAlbEvents.validateIpCondition(event, ''))
178+
.to.deep.equal(['192.168.0.1/21']);
179+
});
180+
});
181+
182+
describe('#validateQueryCondition()', () => {
183+
it('should throw if query is not an object', () => {
184+
const event = { alb: { conditions: { query: 'foo' } } };
185+
expect(() => awsCompileAlbEvents.validateQueryCondition(event, '')).to.throw(Error);
186+
});
187+
188+
it('should return the value if it is an object', () => {
189+
const event = { alb: { conditions: { query: { foo: 'bar' } } } };
190+
expect(awsCompileAlbEvents.validateQueryCondition(event, ''))
191+
.to.deep.equal({ foo: 'bar' });
192+
});
193+
});
194+
195+
describe('#validateHeaderCondition()', () => {
196+
it('should throw if header does not have the required properties', () => {
197+
const event = { alb: { conditions: { header: { name: 'foo', value: 'bar' } } } };
198+
expect(() => awsCompileAlbEvents.validateHeaderCondition(event, '')).to.throw(Error);
199+
});
200+
201+
it('should throw if header.values is not an array', () => {
202+
const event = { alb: { conditions: { header: { name: 'foo', values: 'bar' } } } };
203+
expect(() => awsCompileAlbEvents.validateHeaderCondition(event, '')).to.throw(Error);
204+
});
205+
206+
it('should return the value if it is valid', () => {
207+
const event = { alb: { conditions: { header: { name: 'foo', values: ['bar'] } } } };
208+
expect(awsCompileAlbEvents.validateHeaderCondition(event, ''))
209+
.to.deep.equal({ name: 'foo', values: ['bar'] });
210+
});
211+
});
81212
});

0 commit comments

Comments
 (0)