carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package awsinv
2
3import (
4 "context"
5 "errors"
6 "testing"
7
8 "github.com/aws/aws-sdk-go-v2/aws"
9 "github.com/aws/aws-sdk-go-v2/service/ec2"
10 ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
11 "github.com/aws/aws-sdk-go-v2/service/iam"
12 iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
13 "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
14 rgtypes "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
15 "github.com/aws/aws-sdk-go-v2/service/route53"
16 r53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
17 "github.com/aws/aws-sdk-go-v2/service/s3"
18 s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
19 "github.com/carlosframework/platform/internal/infraview"
20)
21
22type fakeTagging struct {
23 out *resourcegroupstaggingapi.GetResourcesOutput
24 err error
25}
26
27func (f *fakeTagging) GetResources(context.Context, *resourcegroupstaggingapi.GetResourcesInput, ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
28 return f.out, f.err
29}
30
31// A resource the typed mappings already produced must NOT appear twice
32// because the catch-all also saw it; anything the catch-all alone knows
33// about must appear as "other" rather than vanishing. This covers all
34// three global services (S3, Route53, IAM) as well as EC2, because the
35// tagging catch-all runs once per region and a global service's resources
36// surface through whichever region happens to be home for it — a bucket
37// in its own region, a hosted zone or an IAM role through us-east-1 — so a
38// test that wires only EC2 would never exercise the global/regional
39// ordering the catch-all's dedup depends on.
40func TestCatchAllDedupesAgainstTypedResources(t *testing.T) {
41 factory := func(region string) Clients {
42 return Clients{
43 EC2: &fakeEC2{instances: &ec2.DescribeInstancesOutput{
44 Reservations: []ec2types.Reservation{{Instances: []ec2types.Instance{{
45 InstanceId: aws.String("i-123"),
46 }}}},
47 }},
48 S3: &fakeS3{
49 buckets: &s3.ListBucketsOutput{Buckets: []s3types.Bucket{{Name: aws.String("carlos-data-ie")}}},
50 },
51 Route53: &fakeRoute53{
52 zones: &route53.ListHostedZonesOutput{HostedZones: []r53types.HostedZone{
53 {Id: aws.String("/hostedzone/Z1"), Name: aws.String("carlosframework.com.")},
54 }},
55 recs: &route53.ListResourceRecordSetsOutput{},
56 },
57 IAM: &fakeIAM{
58 roles: &iam.ListRolesOutput{Roles: []iamtypes.Role{
59 {RoleName: aws.String("region-host"), Arn: aws.String("arn:aws:iam::1234:role/region-host")},
60 }},
61 users: &iam.ListUsersOutput{},
62 attached: &iam.ListAttachedRolePoliciesOutput{},
63 },
64 Tagging: &fakeTagging{out: &resourcegroupstaggingapi.GetResourcesOutput{
65 ResourceTagMappingList: []rgtypes.ResourceTagMapping{
66 {ResourceARN: aws.String("arn:aws:ec2:eu-west-1:1234:instance/i-123")},
67 {ResourceARN: aws.String("arn:aws:sqs:eu-west-1:1234:some-queue")},
68 {ResourceARN: aws.String("arn:aws:s3:::carlos-data-ie")},
69 {ResourceARN: aws.String("arn:aws:route53:::hostedzone/Z1")},
70 {ResourceARN: aws.String("arn:aws:iam::1234:role/region-host")},
71 },
72 }},
73 }
74 }
75 p := newWithFactory(factory, "eu-west-1", []string{"eu-west-1"}, "1234")
76
77 inv, err := p.Inventory(context.Background())
78 if err != nil {
79 t.Fatalf("Inventory: %v", err)
80 }
81 machines := inv.ByKind(infraview.KindMachine)
82 if len(machines) != 1 {
83 t.Fatalf("machines = %d, want 1 (the catch-all must not duplicate a typed resource)", len(machines))
84 }
85 buckets := inv.ByKind(infraview.KindBucket)
86 if len(buckets) != 1 {
87 t.Fatalf("buckets = %d, want 1 (the catch-all must not duplicate the S3 mapping)", len(buckets))
88 }
89 zones := inv.ByKind(infraview.KindDNSZone)
90 if len(zones) != 1 {
91 t.Fatalf("zones = %d, want 1 (the catch-all must not duplicate the Route53 mapping)", len(zones))
92 }
93 identities := inv.ByKind(infraview.KindIdentity)
94 if len(identities) != 1 {
95 t.Fatalf("identities = %d, want 1 (the catch-all must not duplicate the IAM mapping)", len(identities))
96 }
97 others := inv.ByKind(infraview.KindOther)
98 if len(others) != 1 || others[0].NativeType != "sqs" {
99 t.Fatalf("others = %+v, want one sqs resource", others)
100 }
101 if inv.Provider != "aws" || inv.Account != "1234" || inv.TakenAt.IsZero() {
102 t.Fatalf("header not stamped: %+v", inv)
103 }
104}
105
106// Auth-shaped failure with nothing discovered is the one case that must
107// reach the caller as an error — it is what latches the console's pass off
108// so an ungranted deployment says "not granted" instead of erroring hourly.
109func TestAuthShapedWithNoResourcesIsAnError(t *testing.T) {
110 authErr := &fakeAPIErr{code: "AccessDenied"}
111 factory := func(region string) Clients {
112 return Clients{
113 EC2: &fakeEC2Failing{err: authErr},
114 Tagging: &fakeTagging{err: authErr},
115 }
116 }
117 p := newWithFactory(factory, "eu-west-1", []string{"eu-west-1"}, "")
118 _, err := p.Inventory(context.Background())
119 if err == nil {
120 t.Fatal("want an error when every read is denied and nothing was discovered")
121 }
122 if !errors.Is(err, ErrNoGrant) {
123 t.Fatalf("err = %v, want it to satisfy errors.Is(err, ErrNoGrant) — this is what the console's latch matches on", err)
124 }
125}
126
127// IsAuthShaped must also recognize ErrNoGrant itself, not just a denied
128// smithy code — Inventory's assembly-time denial (this test's own sibling,
129// TestAuthShapedWithNoResourcesIsAnError) has no surviving smithy.APIError
130// to classify by the time it reaches the caller, only this sentinel.
131func TestIsAuthShapedRecognizesErrNoGrant(t *testing.T) {
132 if !IsAuthShaped(ErrNoGrant) {
133 t.Fatal("IsAuthShaped(ErrNoGrant) = false, want true")
134 }
135 if IsAuthShaped(errors.New("some other error")) {
136 t.Fatal("IsAuthShaped(plain error) = true, want false")
137 }
138}
139
140// The same denial, but something WAS discovered: the inventory is returned
141// with the failures named in Partial, never thrown away.
142func TestPartialSurvivesWhenSomethingWasDiscovered(t *testing.T) {
143 factory := func(region string) Clients {
144 return Clients{
145 EC2: &fakeEC2{
146 instances: &ec2.DescribeInstancesOutput{
147 Reservations: []ec2types.Reservation{{Instances: []ec2types.Instance{{
148 InstanceId: aws.String("i-123"),
149 }}}},
150 },
151 errVols: errors.New("AccessDenied: not authorized"),
152 },
153 Tagging: &fakeTagging{err: errors.New("AccessDenied: not authorized")},
154 }
155 }
156 p := newWithFactory(factory, "eu-west-1", []string{"eu-west-1"}, "")
157 inv, err := p.Inventory(context.Background())
158 if err != nil {
159 t.Fatalf("Inventory: %v", err)
160 }
161 if len(inv.Partial) == 0 {
162 t.Fatal("failures must be named in Partial")
163 }
164}
165
166// A tagged resource that happens to share its trailing ARN component with
167// an unrelated typed resource's name must NOT be swallowed by dedup: only
168// a literal Resource.ID match may suppress a catch-all entry. Before this
169// fix, add() also seeded the bare trailing component of every resource's
170// ID into seen, so an IAM role named "worker" silently dropped an SQS
171// queue also named "worker" — a common naming collision, not a rare one.
172func TestCatchAllSurvivesTrailingNameCollision(t *testing.T) {
173 factory := func(region string) Clients {
174 return Clients{
175 IAM: &fakeIAM{
176 roles: &iam.ListRolesOutput{Roles: []iamtypes.Role{
177 {RoleName: aws.String("worker"), Arn: aws.String("arn:aws:iam::1234:role/worker")},
178 }},
179 users: &iam.ListUsersOutput{},
180 attached: &iam.ListAttachedRolePoliciesOutput{},
181 },
182 Tagging: &fakeTagging{out: &resourcegroupstaggingapi.GetResourcesOutput{
183 ResourceTagMappingList: []rgtypes.ResourceTagMapping{
184 {ResourceARN: aws.String("arn:aws:sqs:eu-west-1:1234:worker")},
185 },
186 }},
187 }
188 }
189 p := newWithFactory(factory, "eu-west-1", []string{"eu-west-1"}, "1234")
190
191 inv, err := p.Inventory(context.Background())
192 if err != nil {
193 t.Fatalf("Inventory: %v", err)
194 }
195 identities := inv.ByKind(infraview.KindIdentity)
196 if len(identities) != 1 {
197 t.Fatalf("identities = %d, want 1 (the IAM role)", len(identities))
198 }
199 others := inv.ByKind(infraview.KindOther)
200 if len(others) != 1 || others[0].NativeType != "sqs" {
201 t.Fatalf("others = %+v, want the SQS queue to survive sharing the trailing name %q with the IAM role", others, "worker")
202 }
203}
204