Skip to content

Commit 7bf9602

Browse files
committed
fix: Validate security groups belong to the selected VPC for the ECS Fargate recipes
1 parent 205d8d9 commit 7bf9602

4 files changed

Lines changed: 196 additions & 4 deletions

File tree

src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/SecurityGroupsInVpcValidator.cs

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,17 @@ namespace AWS.Deploy.Common.Recipes.Validation
1616
public class SecurityGroupsInVpcValidator : IOptionSettingItemValidator
1717
{
1818
private static readonly string defaultValidationFailedMessage = "The selected security groups are not part of the selected VPC.";
19+
20+
/// <summary>
21+
/// Path to the OptionSetting that stores a selected Vpc Id
22+
/// </summary>
1923
public string VpcId { get; set; } = "";
24+
25+
/// <summary>
26+
/// Path to the OptionSetting that determines if the default VPC should be used
27+
/// </summary>
28+
public string IsDefaultVpcOptionSettingId { get; set; } = "";
29+
2030
public string ValidationFailedMessage { get; set; } = defaultValidationFailedMessage;
2131

2232
private readonly IAWSResourceQueryer _awsResourceQueryer;
@@ -32,18 +42,77 @@ public async Task<ValidationResult> Validate(object input, Recommendation recomm
3242
{
3343
if (string.IsNullOrEmpty(VpcId))
3444
return ValidationResult.Failed($"The '{nameof(SecurityGroupsInVpcValidator)}' validator is missing the '{nameof(VpcId)}' configuration.");
35-
var vpcIdSetting = _optionSettingHandler.GetOptionSetting(recommendation, VpcId);
36-
var vpcId = _optionSettingHandler.GetOptionSettingValue<string>(recommendation, vpcIdSetting);
45+
46+
var vpcId = "";
47+
48+
// The ECS Fargate recipes expose a separate radio button to select the default VPC which is mutually exclusive
49+
// with specifying an explicit VPC Id. Because we give preference to "UseDefault" in the CDK project,
50+
// we should do so here as well and validate the security groups against the default VPC if it's selected.
51+
if (!string.IsNullOrEmpty(IsDefaultVpcOptionSettingId))
52+
{
53+
var isDefaultVpcOptionSetting = _optionSettingHandler.GetOptionSetting(recommendation, IsDefaultVpcOptionSettingId);
54+
var shouldUseDefaultVpc = _optionSettingHandler.GetOptionSettingValue<bool>(recommendation, isDefaultVpcOptionSetting);
55+
56+
if (shouldUseDefaultVpc)
57+
{
58+
vpcId = (await _awsResourceQueryer.GetDefaultVpc()).VpcId;
59+
}
60+
}
61+
62+
// If the "Use default?" option doesn't exist in the recipe, or it does and was false, or
63+
// we failed to look up the default VPC, then use the explicity VPC Id
64+
if (string.IsNullOrEmpty(vpcId))
65+
{
66+
var vpcIdSetting = _optionSettingHandler.GetOptionSetting(recommendation, VpcId);
67+
vpcId = _optionSettingHandler.GetOptionSettingValue<string>(recommendation, vpcIdSetting);
68+
}
69+
3770
if (string.IsNullOrEmpty(vpcId))
3871
return ValidationResult.Failed("The VpcId setting is not set or is empty. Make sure to set the VPC Id first.");
3972

4073
var securityGroupIds = (await _awsResourceQueryer.DescribeSecurityGroups(vpcId)).Select(x => x.GroupId);
74+
75+
// The ASP.NET Fargate recipe uses a list of security groups
4176
if (input?.TryDeserialize<SortedSet<string>>(out var inputList) ?? false)
4277
{
78+
var invalidSecurityGroups = new List<string>();
4379
foreach (var securityGroup in inputList!)
4480
{
4581
if (!securityGroupIds.Contains(securityGroup))
46-
return ValidationResult.Failed("The selected security group(s) are invalid since they do not belong to the currently selected VPC.");
82+
invalidSecurityGroups.Add(securityGroup);
83+
}
84+
85+
if (invalidSecurityGroups.Any())
86+
{
87+
return ValidationResult.Failed($"The selected security group(s) ({string.Join(", ", invalidSecurityGroups)}) " +
88+
$"are invalid since they do not belong to the currently selected VPC {vpcId}.");
89+
}
90+
91+
return ValidationResult.Valid();
92+
}
93+
94+
// The Console ECS Fargate Service recipe uses a comma-separated string, which will fall through the TryDeserialize above
95+
if (input is string)
96+
{
97+
// Security groups aren't required
98+
if (string.IsNullOrEmpty(input.ToString()))
99+
{
100+
return ValidationResult.Valid();
101+
}
102+
103+
var securityGroupList = input.ToString()?.Split(',') ?? new string[0];
104+
var invalidSecurityGroups = new List<string>();
105+
106+
foreach (var securityGroup in securityGroupList)
107+
{
108+
if (!securityGroupIds.Contains(securityGroup))
109+
invalidSecurityGroups.Add(securityGroup);
110+
}
111+
112+
if (invalidSecurityGroups.Any())
113+
{
114+
return ValidationResult.Failed($"The selected security group(s) ({string.Join(", ", invalidSecurityGroups)}) " +
115+
$"are invalid since they do not belong to the currently selected VPC {vpcId}.");
47116
}
48117

49118
return ValidationResult.Valid();

src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,13 @@
382382
"AllowEmptyString": true,
383383
"ValidationFailedMessage": "Invalid Security Group ID. The Security Group ID must start with the \"sg-\" prefix, followed by either 8 or 17 characters consisting of digits and letters(lower-case) from a to f. For example sg-abc88de9 is a valid Security Group ID."
384384
}
385+
},
386+
{
387+
"ValidatorType": "SecurityGroupsInVpc",
388+
"Configuration": {
389+
"VpcId": "Vpc.VpcId",
390+
"IsDefaultVpcOptionSettingId": "Vpc.IsDefault"
391+
}
385392
}
386393
]
387394
},

src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,16 @@
422422
"Type": "String",
423423
"DefaultValue": "",
424424
"AdvancedSetting": true,
425-
"Updatable": true
425+
"Updatable": true,
426+
"Validators": [
427+
{
428+
"ValidatorType": "SecurityGroupsInVpc",
429+
"Configuration": {
430+
"VpcId": "Vpc.VpcId",
431+
"IsDefaultVpcOptionSettingId": "Vpc.IsDefault"
432+
}
433+
}
434+
]
426435
},
427436
{
428437
"Id": "TaskCpu",

test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using ResourceNotFoundException = Amazon.CloudControlApi.Model.ResourceNotFoundException;
1818
using Task = System.Threading.Tasks.Task;
1919
using System.Collections.Generic;
20+
using Amazon.EC2.Model;
2021

2122
namespace AWS.Deploy.CLI.Common.UnitTests.Recipes.Validation
2223
{
@@ -292,6 +293,48 @@ public async Task DockerExecutionDirectory_AbsoluteDoesNotExist()
292293
await Validate(optionSettingItem, Path.Join("C:", "other_project"), false);
293294
}
294295

296+
/// <summary>
297+
/// Tests the relationship between an explicit VPC ID, whether "Default VPC" is checked,
298+
/// and any security groups that are specified.
299+
/// </summary>
300+
/// <param name="vpcId">selected VPC Id</param>
301+
/// <param name="isDefaultVpcSelected">whether the "Default VPC" radio is selected</param>
302+
/// <param name="selectedSecurityGroups">selected security groups</param>
303+
/// <param name="isValid">Whether or not the test case is expected to be valid</param>
304+
[Theory]
305+
// The Console Service recipe uses a comma-seperated string of security groups
306+
[InlineData("vpc1", true, "", true)] // Valid because the security groups are optional
307+
[InlineData("vpc1", true, "sg-1a,sg-1b", true)] // Valid because the security group does belong to the default VPC
308+
[InlineData("vpc1", true, "sg-1a,sg-2a", false)] // Invalid because the security group does not belong to the default VPC
309+
[InlineData("vpc2", false, "sg-2a", true)] // Valid because the security group does belong to the non-default VPC
310+
[InlineData("vpc2", false, "sg-1a", false)] // Invalid because the security group does not belong to the non-default VPC
311+
[InlineData("vpc2", true, "sg-1a", true)] // Valid because "true" for IsDefaultVPC overrides the "vpc2", so the security group matches
312+
[InlineData("vpc2", true, "sg-2a", false)] // Invalid because "true" for IsDefaultVPC overrides the "vpc2", so the security group does not match
313+
//
314+
// The ASP.NET on Fargate recipe uses a JSON list of security groups (these are same cases from above)
315+
//
316+
[InlineData("vpc1", true, "[]", true)]
317+
[InlineData("vpc1", true, "[\"sg-1a\",\"sg-1b\"]", true)]
318+
[InlineData("vpc1", true, "[\"sg-1a\",\"sg-2a\"]", false)]
319+
[InlineData("vpc2", false, "[\"sg-2a\"]", true)]
320+
[InlineData("vpc2", false, "[\"sg-1a\"]", false)]
321+
[InlineData("vpc2", true, "[\"sg-1a\"]", true)]
322+
[InlineData("vpc2", true, "[\"sg-2a\"]", false)]
323+
324+
public async Task VpcId_DefaultVpc_SecurityGroups_Relationship(string vpcId, bool isDefaultVpcSelected, object selectedSecurityGroups, bool isValid)
325+
{
326+
PrepareMockVPCsAndSecurityGroups(_awsResourceQueryer);
327+
328+
var (vpcIdOption, vpcDefaultOption, securityGroupsOption) = PrepareECSVpcOptions();
329+
330+
securityGroupsOption.Validators.Add(GetSecurityGroupsInVpcValidatorConfig(_awsResourceQueryer, _optionSettingHandler));
331+
332+
await _optionSettingHandler.SetOptionSettingValue(_recommendation, vpcIdOption, vpcId);
333+
await _optionSettingHandler.SetOptionSettingValue(_recommendation, vpcDefaultOption, isDefaultVpcSelected);
334+
335+
await Validate(securityGroupsOption, selectedSecurityGroups, isValid);
336+
}
337+
295338
private OptionSettingItemValidatorConfig GetRegexValidatorConfig(string regex)
296339
{
297340
var regexValidatorConfig = new OptionSettingItemValidatorConfig
@@ -387,5 +430,69 @@ private async Task Validate<T>(OptionSettingItem optionSettingItem, T value, boo
387430
else
388431
exception.ShouldNotBeNull();
389432
}
433+
434+
/// <summary>
435+
/// Prepares a <see cref="SecurityGroupsInVpcValidator"/> for testing
436+
/// </summary>
437+
private OptionSettingItemValidatorConfig GetSecurityGroupsInVpcValidatorConfig(Mock<IAWSResourceQueryer> awsResourceQueryer, IOptionSettingHandler optionSettingHandler)
438+
{
439+
var validator = new SecurityGroupsInVpcValidator(awsResourceQueryer.Object, optionSettingHandler);
440+
validator.VpcId = "Vpc.VpcId";
441+
validator.IsDefaultVpcOptionSettingId = "Vpc.IsDefault";
442+
443+
return new OptionSettingItemValidatorConfig
444+
{
445+
ValidatorType = OptionSettingItemValidatorList.SecurityGroupsInVpc,
446+
Configuration = validator
447+
};
448+
}
449+
450+
/// <summary>
451+
/// Mocks the provided <see cref="IAWSResourceQueryer"> to return the following
452+
/// 1. Default vpc1 with security groups sg-1a and sg-1b
453+
/// 2. Non-default vpc2 with security groups sg-2a and sg-2b
454+
/// </summary>
455+
/// <param name="awsResourceQueryer">Mocked AWS Resource Queryer</param>
456+
private void PrepareMockVPCsAndSecurityGroups(Mock<IAWSResourceQueryer> awsResourceQueryer)
457+
{
458+
awsResourceQueryer.Setup(x => x.GetListOfVpcs()).ReturnsAsync(
459+
new List<Vpc> {
460+
new Vpc { VpcId = "vpc1", IsDefault = true },
461+
new Vpc { VpcId = "vpc2"}
462+
});
463+
464+
awsResourceQueryer.Setup(x => x.DescribeSecurityGroups("vpc1")).ReturnsAsync(
465+
new List<SecurityGroup> {
466+
new SecurityGroup { GroupId = "sg-1a", VpcId = "vpc1" },
467+
new SecurityGroup { GroupId = "sg-1b", VpcId = "vpc1" }
468+
});
469+
470+
awsResourceQueryer.Setup(x => x.DescribeSecurityGroups("vpc2")).ReturnsAsync(
471+
new List<SecurityGroup> {
472+
new SecurityGroup { GroupId = "sg-2a", VpcId = "vpc2" },
473+
new SecurityGroup { GroupId = "sg-2a", VpcId = "vpc2" }
474+
});
475+
476+
awsResourceQueryer.Setup(x => x.GetDefaultVpc()).ReturnsAsync(new Vpc { VpcId = "vpc1", IsDefault = true });
477+
}
478+
479+
/// <summary>
480+
/// Prepares VPC-related options that match the ECS Fargate recipes for testing
481+
/// </summary>
482+
/// <returns>The "Vpc.VpcId" option, the "Vpc.IsDefault" option, and the "ECSServiceSecurityGroups" option</returns>
483+
private (OptionSettingItem, OptionSettingItem, OptionSettingItem) PrepareECSVpcOptions()
484+
{
485+
var vpcIdOption = new OptionSettingItem("VpcId", "Vpc.VpcId", "name", "description");
486+
var vpcDefaultOption = new OptionSettingItem("IsDefault", "Vpc.IsDefault", "name", "description");
487+
var ecsServiceSecurityGroupsOption = new OptionSettingItem("ECSServiceSecurityGroups", "ECSServiceSecurityGroups", "name", "");
488+
489+
var vpc = new OptionSettingItem("Vpc", "Vpc", "", "");
490+
vpc.ChildOptionSettings.Add(vpcIdOption);
491+
vpc.ChildOptionSettings.Add(vpcDefaultOption);
492+
493+
_recipe.OptionSettings.Add(vpc);
494+
495+
return (vpcIdOption, vpcDefaultOption, ecsServiceSecurityGroupsOption);
496+
}
390497
}
391498
}

0 commit comments

Comments
 (0)