mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
feat: implement EC2 GetSecurityGroupsForVpc (#1026)
This commit is contained in:
@@ -22,7 +22,7 @@ Spinifex serves the AWS APIs below. Every page counts the operations in the pinn
|
||||
| Service | Implemented | Modelled | Coverage |
|
||||
|---|---:|---:|---:|
|
||||
| [ACM](/coverage/acm) | 9 | 15 | 60.0% |
|
||||
| [EC2](/coverage/ec2) | 122 | 625 | 19.5% |
|
||||
| [EC2](/coverage/ec2) | 123 | 625 | 19.7% |
|
||||
| [ECR](/coverage/ecr) | 19 | 47 | 40.4% |
|
||||
| [ECS](/coverage/ecs) | 31 | 56 | 55.4% |
|
||||
| [EKS](/coverage/eks) | 34 | 56 | 60.7% |
|
||||
|
||||
@@ -18,7 +18,7 @@ tags:
|
||||
|
||||
## Overview
|
||||
|
||||
Spinifex implements **122 of the 625** operations (**19.5%**) in the EC2 `2016-11-15` API model.
|
||||
Spinifex implements **123 of the 625** operations (**19.7%**) in the EC2 `2016-11-15` API model.
|
||||
|
||||
### Spot Instances Are a Mock
|
||||
|
||||
@@ -492,7 +492,7 @@ Spot Instance Requests are a mock over the on-demand `RunInstances` path. A requ
|
||||
| `GetNetworkInsightsAccessScopeContent` | ❌ Not implemented |
|
||||
| `GetPasswordData` | ✅ Implemented |
|
||||
| `GetReservedInstancesExchangeQuote` | ❌ Not implemented |
|
||||
| `GetSecurityGroupsForVpc` | ❌ Not implemented |
|
||||
| `GetSecurityGroupsForVpc` | ✅ Implemented |
|
||||
| `GetSerialConsoleAccessStatus` | ✅ Implemented |
|
||||
| `GetSnapshotBlockPublicAccessState` | ❌ Not implemented |
|
||||
| `GetSpotPlacementScores` | ❌ Not implemented |
|
||||
|
||||
@@ -970,6 +970,7 @@ func (d *Daemon) subscribeAll() error {
|
||||
{"ec2.DeleteSecurityGroup", handleNATSRequest(d.node, d.vpcService.DeleteSecurityGroup), "spinifex-workers"},
|
||||
{"ec2.DescribeSecurityGroups", handleNATSRequest(d.node, d.vpcService.DescribeSecurityGroups), "spinifex-workers"},
|
||||
{"ec2.DescribeSecurityGroupRules", handleNATSRequest(d.node, d.vpcService.DescribeSecurityGroupRules), "spinifex-workers"},
|
||||
{"ec2.GetSecurityGroupsForVpc", handleNATSRequest(d.node, d.vpcService.GetSecurityGroupsForVpc), "spinifex-workers"},
|
||||
{"ec2.AuthorizeSecurityGroupIngress", handleNATSRequest(d.node, d.vpcService.AuthorizeSecurityGroupIngress), "spinifex-workers"},
|
||||
{"ec2.AuthorizeSecurityGroupEgress", handleNATSRequest(d.node, d.vpcService.AuthorizeSecurityGroupEgress), "spinifex-workers"},
|
||||
{"ec2.RevokeSecurityGroupIngress", handleNATSRequest(d.node, d.vpcService.RevokeSecurityGroupIngress), "spinifex-workers"},
|
||||
|
||||
@@ -535,6 +535,9 @@ var ec2Actions = map[string]ec2Action{
|
||||
"DescribeSecurityGroupRules": ec2Handler(func(ctx context.Context, input *ec2.DescribeSecurityGroupRulesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_vpc.DescribeSecurityGroupRules(ctx, input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"GetSecurityGroupsForVpc": ec2Handler(func(ctx context.Context, input *ec2.GetSecurityGroupsForVpcInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_vpc.GetSecurityGroupsForVpc(ctx, input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"AuthorizeSecurityGroupIngress": ec2Handler(func(ctx context.Context, input *ec2.AuthorizeSecurityGroupIngressInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_vpc.AuthorizeSecurityGroupIngress(ctx, input, gw.NATSConn, accountID)
|
||||
}),
|
||||
|
||||
@@ -214,6 +214,10 @@ var ec2Scopes = map[string][]*resourceScope{
|
||||
"RevokeSecurityGroupIngress": {securityGroupScope},
|
||||
"RevokeSecurityGroupEgress": {securityGroupScope},
|
||||
|
||||
// Unlike the account-wide describes, this one names the VPC it reports on,
|
||||
// so a policy scoped to one VPC has to apply.
|
||||
"GetSecurityGroupsForVpc": {vpcScope},
|
||||
|
||||
// Addresses.
|
||||
"AllocateAddress": {addressNewScope},
|
||||
"ReleaseAddress": {addressScope},
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package gateway_ec2_vpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
handlers_ec2_vpc "github.com/mulgadc/spinifex/spinifex/handlers/ec2/vpc"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// MaxResults bounds from the EC2 model. A raw query request bypasses the
|
||||
// client-side validation an SDK caller gets, so the range is enforced here
|
||||
// rather than silently clamped into a page size the caller did not ask for.
|
||||
const (
|
||||
minSGForVpcResults = 5
|
||||
maxSGForVpcResults = 1000
|
||||
)
|
||||
|
||||
// VpcId is required by the model, so an absent one is MissingParameter before
|
||||
// the request crosses NATS. Whether it names a VPC is the handler's call: only
|
||||
// the store knows.
|
||||
func GetSecurityGroupsForVpc(ctx context.Context, input *ec2.GetSecurityGroupsForVpcInput, natsConn *nats.Conn, accountID string) (ec2.GetSecurityGroupsForVpcOutput, error) {
|
||||
var output ec2.GetSecurityGroupsForVpcOutput
|
||||
if input == nil {
|
||||
return output, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
if input.VpcId == nil || *input.VpcId == "" {
|
||||
return output, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
if input.MaxResults != nil && (*input.MaxResults < minSGForVpcResults || *input.MaxResults > maxSGForVpcResults) {
|
||||
return output, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
svc := handlers_ec2_vpc.NewNATSVPCService(natsConn)
|
||||
result, err := svc.GetSecurityGroupsForVpc(ctx, input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
return *result, nil
|
||||
}
|
||||
@@ -177,6 +177,19 @@ func TestEC2Request_RequestSpotInstancesChecksLaunchResources(t *testing.T) {
|
||||
assertDenied(t, dispatchEC2(t, gw, body))
|
||||
}
|
||||
|
||||
// TestEC2Request_GetSecurityGroupsForVpcIsVpcScoped pins the one read action
|
||||
// that names a resource: registered unscoped, a policy fencing one VPC would be
|
||||
// bypassed by asking about another.
|
||||
func TestEC2Request_GetSecurityGroupsForVpcIsVpcScoped(t *testing.T) {
|
||||
gw := scopedPolicyGateway(
|
||||
statement("Allow", "ec2:*", "*"),
|
||||
statement("Deny", "ec2:GetSecurityGroupsForVpc", "arn:aws:ec2:*:*:vpc/vpc-prod"),
|
||||
)
|
||||
|
||||
assertDenied(t, dispatchEC2(t, gw, "Action=GetSecurityGroupsForVpc&VpcId=vpc-prod"))
|
||||
assertPermitted(t, dispatchEC2(t, gw, "Action=GetSecurityGroupsForVpc&VpcId=vpc-dev"))
|
||||
}
|
||||
|
||||
func TestEC2Request_RejectsOversizedResourceList(t *testing.T) {
|
||||
var body strings.Builder
|
||||
body.WriteString("Action=TerminateInstances")
|
||||
|
||||
@@ -63,6 +63,9 @@ func TestEC2Describe_EmptyResult_EmitsContainer(t *testing.T) {
|
||||
{"DescribeInstanceTypeOfferings", "DescribeInstanceTypeOfferings", "instanceTypeOfferingSet", func(t *testing.T) string {
|
||||
return render[ec2.DescribeInstanceTypeOfferingsInput](t, "DescribeInstanceTypeOfferings", ec2.DescribeInstanceTypeOfferingsOutput{})
|
||||
}},
|
||||
{"GetSecurityGroupsForVpc", "GetSecurityGroupsForVpc", "securityGroupForVpcSet", func(t *testing.T) string {
|
||||
return render[ec2.GetSecurityGroupsForVpcInput](t, "GetSecurityGroupsForVpc", ec2.GetSecurityGroupsForVpcOutput{})
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package handlers_ec2_vpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// groupIDsForVpc collects the ids one GetSecurityGroupsForVpc page reports.
|
||||
func groupIDsForVpc(out *ec2.GetSecurityGroupsForVpcOutput) []string {
|
||||
ids := make([]string, 0, len(out.SecurityGroupForVpcs))
|
||||
for _, g := range out.SecurityGroupForVpcs {
|
||||
ids = append(ids, aws.StringValue(g.GroupId))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// describedGroupIDsForVpc is what DescribeSecurityGroups reports for the same
|
||||
// VPC, so the narrower call can be held against the wider one rather than
|
||||
// against a list the test itself built.
|
||||
func describedGroupIDsForVpc(t *testing.T, svc *VPCServiceImpl, vpcID string) []string {
|
||||
t.Helper()
|
||||
out, err := svc.DescribeSecurityGroups(context.Background(), &ec2.DescribeSecurityGroupsInput{
|
||||
Filters: []*ec2.Filter{{Name: aws.String("vpc-id"), Values: []*string{aws.String(vpcID)}}},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
ids := make([]string, 0, len(out.SecurityGroups))
|
||||
for _, sg := range out.SecurityGroups {
|
||||
ids = append(ids, aws.StringValue(sg.GroupId))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func TestGetSecurityGroupsForVpc_MatchesDescribeForSameVpc(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestVPCService(t)
|
||||
vpcID := createTestVPC(t, svc, "10.0.0.0/16")
|
||||
web := createTestSG(t, svc, vpcID, "web-sg")
|
||||
db := createTestSG(t, svc, vpcID, "db-sg")
|
||||
|
||||
out, err := svc.GetSecurityGroupsForVpc(context.Background(), &ec2.GetSecurityGroupsForVpcInput{
|
||||
VpcId: aws.String(vpcID),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
ids := groupIDsForVpc(out)
|
||||
assert.ElementsMatch(t, describedGroupIDsForVpc(t, svc, vpcID), ids)
|
||||
assert.Contains(t, ids, web)
|
||||
assert.Contains(t, ids, db)
|
||||
assert.Len(t, ids, 3, "the VPC's default group must be reported alongside the two created ones")
|
||||
assert.Nil(t, out.NextToken)
|
||||
|
||||
for _, g := range out.SecurityGroupForVpcs {
|
||||
assert.Equal(t, vpcID, aws.StringValue(g.PrimaryVpcId))
|
||||
assert.Equal(t, testAccountID, aws.StringValue(g.OwnerId))
|
||||
assert.NotEmpty(t, aws.StringValue(g.GroupName))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecurityGroupsForVpc_ExcludesOtherVpcInSameAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestVPCService(t)
|
||||
vpcID := createTestVPC(t, svc, "10.0.0.0/16")
|
||||
otherVpcID := createTestVPC(t, svc, "10.1.0.0/16")
|
||||
mine := createTestSG(t, svc, vpcID, "mine-sg")
|
||||
theirs := createTestSG(t, svc, otherVpcID, "theirs-sg")
|
||||
|
||||
out, err := svc.GetSecurityGroupsForVpc(context.Background(), &ec2.GetSecurityGroupsForVpcInput{
|
||||
VpcId: aws.String(vpcID),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
ids := groupIDsForVpc(out)
|
||||
assert.Contains(t, ids, mine)
|
||||
assert.NotContains(t, ids, theirs)
|
||||
}
|
||||
|
||||
func TestGetSecurityGroupsForVpc_ExcludesOtherAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestVPCService(t)
|
||||
const otherAccountID = "210987654321"
|
||||
|
||||
vpcID := createTestVPC(t, svc, "10.0.0.0/16")
|
||||
mine := createTestSG(t, svc, vpcID, "mine-sg")
|
||||
|
||||
otherVpc, err := svc.CreateVpc(context.Background(), &ec2.CreateVpcInput{
|
||||
CidrBlock: aws.String("10.2.0.0/16"),
|
||||
}, otherAccountID)
|
||||
require.NoError(t, err)
|
||||
otherSG, err := svc.CreateSecurityGroup(context.Background(), &ec2.CreateSecurityGroupInput{
|
||||
GroupName: aws.String("other-account-sg"),
|
||||
Description: aws.String("test sg"),
|
||||
VpcId: otherVpc.Vpc.VpcId,
|
||||
}, otherAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := svc.GetSecurityGroupsForVpc(context.Background(), &ec2.GetSecurityGroupsForVpcInput{
|
||||
VpcId: aws.String(vpcID),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
ids := groupIDsForVpc(out)
|
||||
assert.Contains(t, ids, mine)
|
||||
assert.NotContains(t, ids, aws.StringValue(otherSG.GroupId))
|
||||
|
||||
// The other account's own VPC is invisible to this caller too, rather than
|
||||
// returning that account's groups.
|
||||
_, err = svc.GetSecurityGroupsForVpc(context.Background(), &ec2.GetSecurityGroupsForVpcInput{
|
||||
VpcId: otherVpc.Vpc.VpcId,
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidVpcIDNotFound, err.Error())
|
||||
}
|
||||
|
||||
func TestGetSecurityGroupsForVpc_MissingVpcId(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestVPCService(t)
|
||||
|
||||
_, err := svc.GetSecurityGroupsForVpc(context.Background(), &ec2.GetSecurityGroupsForVpcInput{}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorMissingParameter, err.Error())
|
||||
}
|
||||
|
||||
func TestGetSecurityGroupsForVpc_UnknownVpcIsNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestVPCService(t)
|
||||
createTestVPC(t, svc, "10.0.0.0/16")
|
||||
|
||||
out, err := svc.GetSecurityGroupsForVpc(context.Background(), &ec2.GetSecurityGroupsForVpcInput{
|
||||
VpcId: aws.String("vpc-00000000000000000"),
|
||||
}, testAccountID)
|
||||
require.Error(t, err, "an unknown VPC must not be reported as a VPC with no groups")
|
||||
assert.Nil(t, out)
|
||||
assert.Equal(t, awserrors.ErrorInvalidVpcIDNotFound, err.Error())
|
||||
}
|
||||
|
||||
// A VPC whose only group is the default one still answers, so the not-found
|
||||
// path above cannot be firing on an empty scan result.
|
||||
func TestGetSecurityGroupsForVpc_DefaultGroupOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestVPCService(t)
|
||||
vpcID := createTestVPC(t, svc, "10.0.0.0/16")
|
||||
|
||||
out, err := svc.GetSecurityGroupsForVpc(context.Background(), &ec2.GetSecurityGroupsForVpcInput{
|
||||
VpcId: aws.String(vpcID),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out.SecurityGroupForVpcs, 1)
|
||||
assert.Equal(t, "default", aws.StringValue(out.SecurityGroupForVpcs[0].GroupName))
|
||||
assert.Nil(t, out.NextToken)
|
||||
}
|
||||
|
||||
func TestGetSecurityGroupsForVpc_PagesEveryGroupOnce(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestVPCService(t)
|
||||
vpcID := createTestVPC(t, svc, "10.0.0.0/16")
|
||||
|
||||
expected := describedGroupIDsForVpc(t, svc, vpcID)
|
||||
for _, name := range []string{"sg-a", "sg-b", "sg-c", "sg-d", "sg-e", "sg-f", "sg-g"} {
|
||||
expected = append(expected, createTestSG(t, svc, vpcID, name))
|
||||
}
|
||||
|
||||
var seen []string
|
||||
var token *string
|
||||
for page := 0; ; page++ {
|
||||
require.Less(t, page, 10, "paging did not terminate")
|
||||
|
||||
out, err := svc.GetSecurityGroupsForVpc(context.Background(), &ec2.GetSecurityGroupsForVpcInput{
|
||||
VpcId: aws.String(vpcID),
|
||||
MaxResults: aws.Int64(5),
|
||||
NextToken: token,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.LessOrEqual(t, len(out.SecurityGroupForVpcs), 5)
|
||||
|
||||
seen = append(seen, groupIDsForVpc(out)...)
|
||||
token = out.NextToken
|
||||
if token == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, expected, seen)
|
||||
assert.Len(t, seen, len(expected), "a group was reported on more than one page")
|
||||
}
|
||||
|
||||
func TestGetSecurityGroupsForVpc_MalformedNextToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestVPCService(t)
|
||||
vpcID := createTestVPC(t, svc, "10.0.0.0/16")
|
||||
|
||||
_, err := svc.GetSecurityGroupsForVpc(context.Background(), &ec2.GetSecurityGroupsForVpcInput{
|
||||
VpcId: aws.String(vpcID),
|
||||
NextToken: aws.String("not-a-token"),
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidParameterValue, err.Error())
|
||||
}
|
||||
|
||||
func TestGetSecurityGroupsForVpc_Filters(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestVPCService(t)
|
||||
vpcID := createTestVPC(t, svc, "10.0.0.0/16")
|
||||
web := createTestSG(t, svc, vpcID, "web-sg")
|
||||
createTestSG(t, svc, vpcID, "db-sg")
|
||||
|
||||
out, err := svc.GetSecurityGroupsForVpc(context.Background(), &ec2.GetSecurityGroupsForVpcInput{
|
||||
VpcId: aws.String(vpcID),
|
||||
Filters: []*ec2.Filter{{Name: aws.String("group-name"), Values: []*string{aws.String("web-sg")}}},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{web}, groupIDsForVpc(out))
|
||||
|
||||
// vpc-id is DescribeSecurityGroups' name for it; this model says
|
||||
// primary-vpc-id, and an unsupported filter must not be ignored.
|
||||
_, err = svc.GetSecurityGroupsForVpc(context.Background(), &ec2.GetSecurityGroupsForVpcInput{
|
||||
VpcId: aws.String(vpcID),
|
||||
Filters: []*ec2.Filter{{Name: aws.String("vpc-id"), Values: []*string{aws.String(vpcID)}}},
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidParameterValue, err.Error())
|
||||
|
||||
out, err = svc.GetSecurityGroupsForVpc(context.Background(), &ec2.GetSecurityGroupsForVpcInput{
|
||||
VpcId: aws.String(vpcID),
|
||||
Filters: []*ec2.Filter{{Name: aws.String("primary-vpc-id"), Values: []*string{aws.String(vpcID)}}},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, out.SecurityGroupForVpcs, 3)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -533,6 +534,170 @@ func sgIngressCIDRMatchesAny(rules []SGRule, values []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// getSecurityGroupsForVpcValidFilters defines the set of filter names accepted
|
||||
// by GetSecurityGroupsForVpc. The model names the VPC primary-vpc-id here, and
|
||||
// does not accept the vpc-id or ip-permission.cidr DescribeSecurityGroups takes.
|
||||
var getSecurityGroupsForVpcValidFilters = map[string]bool{
|
||||
"group-id": true,
|
||||
"group-name": true,
|
||||
"description": true,
|
||||
"owner-id": true,
|
||||
"primary-vpc-id": true,
|
||||
}
|
||||
|
||||
// defaultSGForVpcPerPage caps a request that names no MaxResults, matching the
|
||||
// model's ceiling.
|
||||
const defaultSGForVpcPerPage = 1000
|
||||
|
||||
// GetSecurityGroupsForVpc lists the security groups an interface in the named
|
||||
// VPC can be associated with. It is the same account-prefixed scan
|
||||
// DescribeSecurityGroups runs, projected to the fields the model returns: the
|
||||
// rule set is deliberately absent, since a caller wanting rules has
|
||||
// DescribeSecurityGroupRules.
|
||||
func (s *VPCServiceImpl) GetSecurityGroupsForVpc(ctx context.Context, input *ec2.GetSecurityGroupsForVpcInput, accountID string) (*ec2.GetSecurityGroupsForVpcOutput, error) {
|
||||
if input == nil || input.VpcId == nil || *input.VpcId == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
vpcID := *input.VpcId
|
||||
|
||||
// An unknown VPC is not-found rather than an empty list: an empty list is
|
||||
// indistinguishable from a VPC that genuinely holds no groups, and would
|
||||
// send the caller on to create an interface in a VPC that does not exist.
|
||||
if err := s.requireVPCExists(ctx, accountID, vpcID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsedFilters, err := filterutil.ParseFilters(input.Filters, getSecurityGroupsForVpcValidFilters)
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx, "GetSecurityGroupsForVpc: invalid filter", "err", err)
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
prefix := accountID + "."
|
||||
keys, err := s.sgKV.Keys(ctx)
|
||||
if err != nil && !errors.Is(err, jetstream.ErrNoKeysFound) {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
groups := []*ec2.SecurityGroupForVpc{}
|
||||
for _, key := range keys {
|
||||
if key == utils.VersionKey || !strings.HasPrefix(key, prefix) {
|
||||
continue
|
||||
}
|
||||
entry, err := s.sgKV.Get(ctx, key)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "GetSecurityGroupsForVpc: SG read failed", "key", key, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
var record SecurityGroupRecord
|
||||
if err := json.Unmarshal(entry.Value(), &record); err != nil {
|
||||
slog.ErrorContext(ctx, "GetSecurityGroupsForVpc: SG unmarshal failed", "key", key, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
if record.VpcId != vpcID {
|
||||
continue
|
||||
}
|
||||
if len(parsedFilters) > 0 && !sgForVpcMatchesFilters(&record, accountID, parsedFilters) {
|
||||
continue
|
||||
}
|
||||
|
||||
groups = append(groups, &ec2.SecurityGroupForVpc{
|
||||
GroupId: aws.String(record.GroupId),
|
||||
GroupName: aws.String(record.GroupName),
|
||||
Description: aws.String(record.Description),
|
||||
OwnerId: aws.String(accountID),
|
||||
PrimaryVpcId: aws.String(record.VpcId),
|
||||
Tags: utils.MapToEC2Tags(record.Tags),
|
||||
})
|
||||
}
|
||||
|
||||
// The KV key order is not guaranteed stable across calls, so a token
|
||||
// addressing an offset into an unsorted set could skip or repeat a group.
|
||||
slices.SortFunc(groups, func(a, b *ec2.SecurityGroupForVpc) int {
|
||||
return strings.Compare(aws.StringValue(a.GroupId), aws.StringValue(b.GroupId))
|
||||
})
|
||||
|
||||
page, nextToken, err := pageSecurityGroupsForVpc(groups, input.NextToken, input.MaxResults)
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx, "GetSecurityGroupsForVpc: invalid NextToken", "next_token", aws.StringValue(input.NextToken))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "GetSecurityGroupsForVpc completed",
|
||||
"count", len(page), "total", len(groups), "vpcId", vpcID, "accountID", accountID)
|
||||
|
||||
return &ec2.GetSecurityGroupsForVpcOutput{
|
||||
SecurityGroupForVpcs: page,
|
||||
NextToken: nextToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// sgForVpcMatchesFilters applies the GetSecurityGroupsForVpc filter set. The
|
||||
// owner is the account the scan is prefixed by, so owner-id can only match the
|
||||
// caller's own account.
|
||||
func sgForVpcMatchesFilters(record *SecurityGroupRecord, accountID string, filters map[string][]string) bool {
|
||||
for name, values := range filters {
|
||||
if strings.HasPrefix(name, "tag:") {
|
||||
continue
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "group-id":
|
||||
if !filterutil.MatchesAny(values, record.GroupId) {
|
||||
return false
|
||||
}
|
||||
case "group-name":
|
||||
if !filterutil.MatchesAny(values, record.GroupName) {
|
||||
return false
|
||||
}
|
||||
case "description":
|
||||
if !filterutil.MatchesAny(values, record.Description) {
|
||||
return false
|
||||
}
|
||||
case "owner-id":
|
||||
if !filterutil.MatchesAny(values, accountID) {
|
||||
return false
|
||||
}
|
||||
case "primary-vpc-id":
|
||||
if !filterutil.MatchesAny(values, record.VpcId) {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return filterutil.MatchesTags(filters, record.Tags)
|
||||
}
|
||||
|
||||
// pageSecurityGroupsForVpc slices one page out of a sorted group set, using an
|
||||
// opaque integer offset as the token. The last page carries a nil token.
|
||||
func pageSecurityGroupsForVpc(groups []*ec2.SecurityGroupForVpc, token *string, maxResults *int64) ([]*ec2.SecurityGroupForVpc, *string, error) {
|
||||
start := 0
|
||||
if aws.StringValue(token) != "" {
|
||||
n, err := strconv.Atoi(*token)
|
||||
if err != nil || n < 0 {
|
||||
return nil, nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
start = n
|
||||
}
|
||||
if start > len(groups) {
|
||||
start = len(groups)
|
||||
}
|
||||
|
||||
size := defaultSGForVpcPerPage
|
||||
if maxResults != nil && *maxResults > 0 {
|
||||
size = int(*maxResults)
|
||||
}
|
||||
|
||||
end := start + size
|
||||
if end >= len(groups) {
|
||||
return groups[start:], nil, nil
|
||||
}
|
||||
return groups[start:end], aws.String(strconv.Itoa(end)), nil
|
||||
}
|
||||
|
||||
// describeSecurityGroupRulesValidFilters defines the set of filter names accepted by DescribeSecurityGroupRules.
|
||||
var describeSecurityGroupRulesValidFilters = map[string]bool{
|
||||
"group-id": true,
|
||||
|
||||
@@ -25,6 +25,7 @@ type VPCService interface {
|
||||
DeleteSecurityGroup(ctx context.Context, input *ec2.DeleteSecurityGroupInput, accountID string) (*ec2.DeleteSecurityGroupOutput, error)
|
||||
DescribeSecurityGroups(ctx context.Context, input *ec2.DescribeSecurityGroupsInput, accountID string) (*ec2.DescribeSecurityGroupsOutput, error)
|
||||
DescribeSecurityGroupRules(ctx context.Context, input *ec2.DescribeSecurityGroupRulesInput, accountID string) (*ec2.DescribeSecurityGroupRulesOutput, error)
|
||||
GetSecurityGroupsForVpc(ctx context.Context, input *ec2.GetSecurityGroupsForVpcInput, accountID string) (*ec2.GetSecurityGroupsForVpcOutput, error)
|
||||
AuthorizeSecurityGroupIngress(ctx context.Context, input *ec2.AuthorizeSecurityGroupIngressInput, accountID string) (*ec2.AuthorizeSecurityGroupIngressOutput, error)
|
||||
AuthorizeSecurityGroupEgress(ctx context.Context, input *ec2.AuthorizeSecurityGroupEgressInput, accountID string) (*ec2.AuthorizeSecurityGroupEgressOutput, error)
|
||||
RevokeSecurityGroupIngress(ctx context.Context, input *ec2.RevokeSecurityGroupIngressInput, accountID string) (*ec2.RevokeSecurityGroupIngressOutput, error)
|
||||
|
||||
@@ -89,6 +89,10 @@ func (s *NATSVPCService) DescribeSecurityGroupRules(ctx context.Context, input *
|
||||
return utils.NATSRequest[ec2.DescribeSecurityGroupRulesOutput](ctx, s.natsConn, "ec2.DescribeSecurityGroupRules", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSVPCService) GetSecurityGroupsForVpc(ctx context.Context, input *ec2.GetSecurityGroupsForVpcInput, accountID string) (*ec2.GetSecurityGroupsForVpcOutput, error) {
|
||||
return utils.NATSRequest[ec2.GetSecurityGroupsForVpcOutput](ctx, s.natsConn, "ec2.GetSecurityGroupsForVpc", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSVPCService) AuthorizeSecurityGroupIngress(ctx context.Context, input *ec2.AuthorizeSecurityGroupIngressInput, accountID string) (*ec2.AuthorizeSecurityGroupIngressOutput, error) {
|
||||
return utils.NATSRequest[ec2.AuthorizeSecurityGroupIngressOutput](ctx, s.natsConn, "ec2.AuthorizeSecurityGroupIngress", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user