From 9d1b2c7b1f0147089f912c32a61d7ba86edb543c Mon Sep 17 00:00:00 2001 From: Jacob Klitzke Date: Wed, 19 Jan 2022 10:43:36 -0800 Subject: [PATCH] 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* --- packages/@aws-cdk/aws-ec2/lib/peer.ts | 56 ++++++++ .../aws-ec2/test/security-group.test.ts | 120 ++++++++++++++++++ 2 files changed, 176 insertions(+) diff --git a/packages/@aws-cdk/aws-ec2/lib/peer.ts b/packages/@aws-cdk/aws-ec2/lib/peer.ts index 333bd66bc91a9..2c0cc4be394c4 100644 --- a/packages/@aws-cdk/aws-ec2/lib/peer.ts +++ b/packages/@aws-cdk/aws-ec2/lib/peer.ts @@ -75,6 +75,13 @@ export class Peer { return new PrefixList(prefixListId); } + /** + * A security group ID + */ + public static securityGroupId(securityGroupId: string, sourceSecurityGroupOwnerId?: string): IPeer { + return new SecurityGroupId(securityGroupId, sourceSecurityGroupOwnerId); + } + protected constructor() { } } @@ -199,3 +206,52 @@ class PrefixList implements IPeer { return { destinationPrefixListId: this.prefixListId }; } } + +/** + * A connection to or from a given security group ID + * + * For ingress rules, a sourceSecurityGroupOwnerId parameter can be specified if + * the security group exists in another account. + * This parameter will be ignored for egress rules. + */ +class SecurityGroupId implements IPeer { + public readonly canInlineRule = true; + public readonly connections: Connections = new Connections({ peer: this }); + public readonly uniqueId: string; + + constructor(private readonly securityGroupId: string, private readonly sourceSecurityGroupOwnerId?: string) { + if (!Token.isUnresolved(securityGroupId)) { + const securityGroupMatch = securityGroupId.match(/^sg-[a-z0-9]{8,17}$/); + + if (!securityGroupMatch) { + throw new Error(`Invalid security group ID: "${securityGroupId}"`); + } + } + + if (sourceSecurityGroupOwnerId && !Token.isUnresolved(sourceSecurityGroupOwnerId)) { + const accountNumberMatch = sourceSecurityGroupOwnerId.match(/^[0-9]{12}$/); + + if (!accountNumberMatch) { + throw new Error(`Invalid security group owner ID: "${sourceSecurityGroupOwnerId}"`); + } + } + this.uniqueId = securityGroupId; + } + + /** + * Produce the ingress rule JSON for the given connection + */ + public toIngressRuleConfig(): any { + return { + sourceSecurityGroupId: this.securityGroupId, + ...(this.sourceSecurityGroupOwnerId && { sourceSecurityGroupOwnerId: this.sourceSecurityGroupOwnerId }), + }; + } + + /** + * Produce the egress rule JSON for the given connection + */ + public toEgressRuleConfig(): any { + return { destinationSecurityGroupId: this.securityGroupId }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts index c7c3662f8592f..f4bdd13556457 100644 --- a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts @@ -178,6 +178,7 @@ describe('security group', () => { Peer.anyIpv4(), Peer.anyIpv6(), Peer.prefixList('pl-012345'), + Peer.securityGroupId('sg-012345678'), ]; const ports = [ @@ -337,6 +338,125 @@ describe('security group', () => { }); }); + describe('Peer security group ID validation', () => { + test('passes with valid security group ID', () => { + //GIVEN + const securityGroupIds = ['sg-12345678', 'sg-0123456789abcdefg']; + + // THEN + for (const securityGroupId of securityGroupIds) { + expect(Peer.securityGroupId(securityGroupId).uniqueId).toEqual(securityGroupId); + } + }); + + test('passes with valid security group ID and source owner id', () => { + //GIVEN + const securityGroupIds = ['sg-12345678', 'sg-0123456789abcdefg']; + const ownerIds = ['000000000000', '000000000001']; + + // THEN + for (const securityGroupId of securityGroupIds) { + for (const ownerId of ownerIds) { + expect(Peer.securityGroupId(securityGroupId, ownerId).uniqueId).toEqual(securityGroupId); + } + } + }); + + test('passes with unresolved security group id token or owner id token', () => { + // GIVEN + Token.asString('securityGroupId'); + + const securityGroupId = Lazy.string({ produce: () => 'sg-01234567' }); + const ownerId = Lazy.string({ produce: () => '000000000000' }); + Peer.securityGroupId(securityGroupId); + Peer.securityGroupId(securityGroupId, ownerId); + + // THEN: don't throw + }); + + test('throws if invalid security group ID', () => { + // THEN + expect(() => { + Peer.securityGroupId('invalid'); + }).toThrow(/Invalid security group ID/); + + + }); + + test('throws if invalid source security group id', () => { + // THEN + expect(() => { + Peer.securityGroupId('sg-12345678', 'invalid'); + }).toThrow(/Invalid security group owner ID/); + }); + }); + + describe('SourceSecurityGroupOwnerId property validation', () => { + test('SourceSecurityGroupOwnerId property is not present when value is not provided to ingress rule', () => { + // GIVEN + const stack = new Stack(undefined, 'TestStack'); + const vpc = new Vpc(stack, 'VPC'); + const sg = new SecurityGroup(stack, 'SG', { vpc }); + + //WHEN + sg.addIngressRule(Peer.securityGroupId('sg-123456789'), Port.allTcp(), 'no owner id property'); + + //THEN + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: [{ + SourceSecurityGroupId: 'sg-123456789', + Description: 'no owner id property', + FromPort: 0, + ToPort: 65535, + IpProtocol: 'tcp', + }], + }); + }); + + test('SourceSecurityGroupOwnerId property is present when value is provided to ingress rule', () => { + // GIVEN + const stack = new Stack(undefined, 'TestStack'); + const vpc = new Vpc(stack, 'VPC'); + const sg = new SecurityGroup(stack, 'SG', { vpc }); + + //WHEN + sg.addIngressRule(Peer.securityGroupId('sg-123456789', '000000000000'), Port.allTcp(), 'contains owner id property'); + + //THEN + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: [{ + SourceSecurityGroupId: 'sg-123456789', + SourceSecurityGroupOwnerId: '000000000000', + Description: 'contains owner id property', + FromPort: 0, + ToPort: 65535, + IpProtocol: 'tcp', + }], + }); + }); + + test('SourceSecurityGroupOwnerId property is not present when value is provided to egress rule', () => { + // GIVEN + const stack = new Stack(undefined, 'TestStack'); + const vpc = new Vpc(stack, 'VPC'); + const sg = new SecurityGroup(stack, 'SG', { vpc, allowAllOutbound: false }); + + //WHEN + sg.addEgressRule(Peer.securityGroupId('sg-123456789', '000000000000'), Port.allTcp(), 'no owner id property'); + + //THEN + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [{ + DestinationSecurityGroupId: 'sg-123456789', + Description: 'no owner id property', + FromPort: 0, + ToPort: 65535, + IpProtocol: 'tcp', + }], + }); + }); + }); + testDeprecated('can look up a security group', () => { const app = new App(); const stack = new Stack(app, 'stack', {