Skip to content

Commit 9d1b2c7

Browse files
authored
feat(ec2): create Peers via security group ids (#18248)
Allows users to add ingress/egress security group rules containing a security group id using the Peer interface. Implements #7111 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent eb29e6f commit 9d1b2c7

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed

packages/@aws-cdk/aws-ec2/lib/peer.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ export class Peer {
7575
return new PrefixList(prefixListId);
7676
}
7777

78+
/**
79+
* A security group ID
80+
*/
81+
public static securityGroupId(securityGroupId: string, sourceSecurityGroupOwnerId?: string): IPeer {
82+
return new SecurityGroupId(securityGroupId, sourceSecurityGroupOwnerId);
83+
}
84+
7885
protected constructor() {
7986
}
8087
}
@@ -199,3 +206,52 @@ class PrefixList implements IPeer {
199206
return { destinationPrefixListId: this.prefixListId };
200207
}
201208
}
209+
210+
/**
211+
* A connection to or from a given security group ID
212+
*
213+
* For ingress rules, a sourceSecurityGroupOwnerId parameter can be specified if
214+
* the security group exists in another account.
215+
* This parameter will be ignored for egress rules.
216+
*/
217+
class SecurityGroupId implements IPeer {
218+
public readonly canInlineRule = true;
219+
public readonly connections: Connections = new Connections({ peer: this });
220+
public readonly uniqueId: string;
221+
222+
constructor(private readonly securityGroupId: string, private readonly sourceSecurityGroupOwnerId?: string) {
223+
if (!Token.isUnresolved(securityGroupId)) {
224+
const securityGroupMatch = securityGroupId.match(/^sg-[a-z0-9]{8,17}$/);
225+
226+
if (!securityGroupMatch) {
227+
throw new Error(`Invalid security group ID: "${securityGroupId}"`);
228+
}
229+
}
230+
231+
if (sourceSecurityGroupOwnerId && !Token.isUnresolved(sourceSecurityGroupOwnerId)) {
232+
const accountNumberMatch = sourceSecurityGroupOwnerId.match(/^[0-9]{12}$/);
233+
234+
if (!accountNumberMatch) {
235+
throw new Error(`Invalid security group owner ID: "${sourceSecurityGroupOwnerId}"`);
236+
}
237+
}
238+
this.uniqueId = securityGroupId;
239+
}
240+
241+
/**
242+
* Produce the ingress rule JSON for the given connection
243+
*/
244+
public toIngressRuleConfig(): any {
245+
return {
246+
sourceSecurityGroupId: this.securityGroupId,
247+
...(this.sourceSecurityGroupOwnerId && { sourceSecurityGroupOwnerId: this.sourceSecurityGroupOwnerId }),
248+
};
249+
}
250+
251+
/**
252+
* Produce the egress rule JSON for the given connection
253+
*/
254+
public toEgressRuleConfig(): any {
255+
return { destinationSecurityGroupId: this.securityGroupId };
256+
}
257+
}

packages/@aws-cdk/aws-ec2/test/security-group.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ describe('security group', () => {
178178
Peer.anyIpv4(),
179179
Peer.anyIpv6(),
180180
Peer.prefixList('pl-012345'),
181+
Peer.securityGroupId('sg-012345678'),
181182
];
182183

183184
const ports = [
@@ -337,6 +338,125 @@ describe('security group', () => {
337338
});
338339
});
339340

341+
describe('Peer security group ID validation', () => {
342+
test('passes with valid security group ID', () => {
343+
//GIVEN
344+
const securityGroupIds = ['sg-12345678', 'sg-0123456789abcdefg'];
345+
346+
// THEN
347+
for (const securityGroupId of securityGroupIds) {
348+
expect(Peer.securityGroupId(securityGroupId).uniqueId).toEqual(securityGroupId);
349+
}
350+
});
351+
352+
test('passes with valid security group ID and source owner id', () => {
353+
//GIVEN
354+
const securityGroupIds = ['sg-12345678', 'sg-0123456789abcdefg'];
355+
const ownerIds = ['000000000000', '000000000001'];
356+
357+
// THEN
358+
for (const securityGroupId of securityGroupIds) {
359+
for (const ownerId of ownerIds) {
360+
expect(Peer.securityGroupId(securityGroupId, ownerId).uniqueId).toEqual(securityGroupId);
361+
}
362+
}
363+
});
364+
365+
test('passes with unresolved security group id token or owner id token', () => {
366+
// GIVEN
367+
Token.asString('securityGroupId');
368+
369+
const securityGroupId = Lazy.string({ produce: () => 'sg-01234567' });
370+
const ownerId = Lazy.string({ produce: () => '000000000000' });
371+
Peer.securityGroupId(securityGroupId);
372+
Peer.securityGroupId(securityGroupId, ownerId);
373+
374+
// THEN: don't throw
375+
});
376+
377+
test('throws if invalid security group ID', () => {
378+
// THEN
379+
expect(() => {
380+
Peer.securityGroupId('invalid');
381+
}).toThrow(/Invalid security group ID/);
382+
383+
384+
});
385+
386+
test('throws if invalid source security group id', () => {
387+
// THEN
388+
expect(() => {
389+
Peer.securityGroupId('sg-12345678', 'invalid');
390+
}).toThrow(/Invalid security group owner ID/);
391+
});
392+
});
393+
394+
describe('SourceSecurityGroupOwnerId property validation', () => {
395+
test('SourceSecurityGroupOwnerId property is not present when value is not provided to ingress rule', () => {
396+
// GIVEN
397+
const stack = new Stack(undefined, 'TestStack');
398+
const vpc = new Vpc(stack, 'VPC');
399+
const sg = new SecurityGroup(stack, 'SG', { vpc });
400+
401+
//WHEN
402+
sg.addIngressRule(Peer.securityGroupId('sg-123456789'), Port.allTcp(), 'no owner id property');
403+
404+
//THEN
405+
expect(stack).toHaveResource('AWS::EC2::SecurityGroup', {
406+
SecurityGroupIngress: [{
407+
SourceSecurityGroupId: 'sg-123456789',
408+
Description: 'no owner id property',
409+
FromPort: 0,
410+
ToPort: 65535,
411+
IpProtocol: 'tcp',
412+
}],
413+
});
414+
});
415+
416+
test('SourceSecurityGroupOwnerId property is present when value is provided to ingress rule', () => {
417+
// GIVEN
418+
const stack = new Stack(undefined, 'TestStack');
419+
const vpc = new Vpc(stack, 'VPC');
420+
const sg = new SecurityGroup(stack, 'SG', { vpc });
421+
422+
//WHEN
423+
sg.addIngressRule(Peer.securityGroupId('sg-123456789', '000000000000'), Port.allTcp(), 'contains owner id property');
424+
425+
//THEN
426+
expect(stack).toHaveResource('AWS::EC2::SecurityGroup', {
427+
SecurityGroupIngress: [{
428+
SourceSecurityGroupId: 'sg-123456789',
429+
SourceSecurityGroupOwnerId: '000000000000',
430+
Description: 'contains owner id property',
431+
FromPort: 0,
432+
ToPort: 65535,
433+
IpProtocol: 'tcp',
434+
}],
435+
});
436+
});
437+
438+
test('SourceSecurityGroupOwnerId property is not present when value is provided to egress rule', () => {
439+
// GIVEN
440+
const stack = new Stack(undefined, 'TestStack');
441+
const vpc = new Vpc(stack, 'VPC');
442+
const sg = new SecurityGroup(stack, 'SG', { vpc, allowAllOutbound: false });
443+
444+
//WHEN
445+
sg.addEgressRule(Peer.securityGroupId('sg-123456789', '000000000000'), Port.allTcp(), 'no owner id property');
446+
447+
//THEN
448+
expect(stack).toHaveResource('AWS::EC2::SecurityGroup', {
449+
SecurityGroupEgress: [{
450+
DestinationSecurityGroupId: 'sg-123456789',
451+
Description: 'no owner id property',
452+
FromPort: 0,
453+
ToPort: 65535,
454+
IpProtocol: 'tcp',
455+
}],
456+
});
457+
});
458+
});
459+
340460
testDeprecated('can look up a security group', () => {
341461
const app = new App();
342462
const stack = new Stack(app, 'stack', {

0 commit comments

Comments
 (0)