Skip to content

Commit 7c7f522

Browse files
authored
fix: access graph fails to fetch s3 bucket details (#50758) (#50765)
* fix: access graph fails to fetch s3 bucket details With SDK V1, AWS get bucket details such as policies, acls, status... didn't require the specification of the target s3 bucket location. With the recent changes to support newer versions of the AWS SDK, the get bucket details started to fail with the following error: ``` BucketRegionError: incorrect region, the bucket is not in 'ap-south-1' region at endpoint '', bucket is in 'eu-central-1' region status code: 301, request id: QS5C24H12ZV3VNM4, host id: mzVDk4010MPTCFdxyE/XwERX9W35MSge85PG+h5Jvwyvi7MhxdXLaysb2PTZCMY9r1ngBi6Gv6g=, failed to fetch bucket "cyz" acls polic ``` This PR uses `HeadBucket` to retrieve the location of the s3 bucket and then uses the bucket location client to retrieve the s3 bucket details. Fixes #50757 * fix tests * handle review comments
1 parent 7cc429b commit 7c7f522

File tree

5 files changed

+157
-57
lines changed

5 files changed

+157
-57
lines changed

docs/pages/admin-guides/teleport-policy/integrations/aws-sync.mdx

+2
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ The IAM policy includes the following directives:
157157
"s3:ListBucket",
158158
"s3:GetBucketLocation",
159159
"s3:GetBucketTagging",
160+
"s3:GetBucketPolicyStatus",
161+
"s3:GetBucketAcl",
160162

161163
"iam:ListUsers",
162164
"iam:GetUser",

lib/cloud/aws/policy_statements.go

+3
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,9 @@ func StatementAccessGraphAWSSync() *Statement {
410410
"s3:ListBucket",
411411
"s3:GetBucketLocation",
412412
"s3:GetBucketTagging",
413+
"s3:GetBucketPolicyStatus",
414+
"s3:GetBucketAcl",
415+
413416
// IAM IAM
414417
"iam:ListUsers",
415418
"iam:GetUser",

lib/cloud/mocks/aws_s3.go

+14
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type S3Mock struct {
3535
BucketPolicyStatus map[string]*s3.PolicyStatus
3636
BucketACL map[string][]*s3.Grant
3737
BucketTags map[string][]*s3.Tag
38+
BucketLocations map[string]string
3839
}
3940

4041
func (m *S3Mock) ListBucketsWithContext(_ aws.Context, _ *s3.ListBucketsInput, _ ...request.Option) (*s3.ListBucketsOutput, error) {
@@ -94,3 +95,16 @@ func (m *S3Mock) GetBucketTaggingWithContext(_ aws.Context, input *s3.GetBucketT
9495
TagSet: tags,
9596
}, nil
9697
}
98+
99+
func (m *S3Mock) GetBucketLocationWithContext(_ aws.Context, input *s3.GetBucketLocationInput, _ ...request.Option) (*s3.GetBucketLocationOutput, error) {
100+
if aws.StringValue(input.Bucket) == "" {
101+
return nil, trace.BadParameter("incorrect bucket name")
102+
}
103+
location, ok := m.BucketLocations[aws.StringValue(input.Bucket)]
104+
if !ok {
105+
return nil, trace.NotFound("bucket %v not found", aws.StringValue(input.Bucket))
106+
}
107+
return &s3.GetBucketLocationOutput{
108+
LocationConstraint: aws.String(location),
109+
}, nil
110+
}

lib/srv/discovery/fetchers/aws-sync/s3.go

+134-57
Original file line numberDiff line numberDiff line change
@@ -71,28 +71,13 @@ func (a *awsFetcher) fetchS3Buckets(ctx context.Context) ([]*accessgraphv1alpha.
7171
}
7272
}
7373

74-
region := awsutil.GetKnownRegions()[0]
75-
if len(a.Regions) > 0 {
76-
region = a.Regions[0]
77-
}
78-
79-
s3Client, err := a.CloudClients.GetAWSS3Client(
80-
ctx,
81-
region,
82-
a.getAWSOptions()...,
83-
)
84-
if err != nil {
85-
return nil, trace.Wrap(err)
86-
}
87-
rsp, err := s3Client.ListBucketsWithContext(
88-
ctx,
89-
&s3.ListBucketsInput{},
90-
)
74+
buckets, getBucketRegion, err := a.listS3Buckets(ctx)
9175
if err != nil {
9276
return existing.S3Buckets, trace.Wrap(err)
9377
}
9478

95-
for _, bucket := range rsp.Buckets {
79+
// Iterate over the buckets and fetch their inline and attached policies.
80+
for _, bucket := range buckets {
9681
bucket := bucket
9782
eG.Go(func() error {
9883
var failedReqs failedRequests
@@ -101,54 +86,24 @@ func (a *awsFetcher) fetchS3Buckets(ctx context.Context) ([]*accessgraphv1alpha.
10186
return b.Name == aws.ToString(bucket.Name) && b.AccountId == a.AccountID
10287
},
10388
)
104-
policy, err := s3Client.GetBucketPolicyWithContext(ctx, &s3.GetBucketPolicyInput{
105-
Bucket: bucket.Name,
106-
})
89+
bucketRegion, err := getBucketRegion(bucket.Name)
10790
if err != nil {
10891
errs = append(errs,
109-
trace.Wrap(err, "failed to fetch bucket %q inline policy", aws.ToString(bucket.Name)),
92+
trace.Wrap(err),
11093
)
11194
failedReqs.policyFailed = true
112-
}
113-
114-
policyStatus, err := s3Client.GetBucketPolicyStatusWithContext(ctx, &s3.GetBucketPolicyStatusInput{
115-
Bucket: bucket.Name,
116-
})
117-
if err != nil {
118-
errs = append(errs,
119-
trace.Wrap(err, "failed to fetch bucket %q policy status", aws.ToString(bucket.Name)),
120-
)
12195
failedReqs.failedPolicyStatus = true
122-
}
123-
124-
acls, err := s3Client.GetBucketAclWithContext(ctx, &s3.GetBucketAclInput{
125-
Bucket: bucket.Name,
126-
})
127-
if err != nil {
128-
errs = append(errs,
129-
trace.Wrap(err, "failed to fetch bucket %q acls policies", aws.ToString(bucket.Name)),
130-
)
13196
failedReqs.failedAcls = true
132-
}
133-
134-
tagsOutput, err := s3Client.GetBucketTaggingWithContext(ctx, &s3.GetBucketTaggingInput{
135-
Bucket: bucket.Name,
136-
})
137-
var awsErr awserr.Error
138-
const noSuchTagSet = "NoSuchTagSet" // error code when there are no tags or the bucket does not support them
139-
if errors.As(err, &awsErr) && awsErr.Code() == noSuchTagSet {
140-
// If there are no tags, set the error to nil.
141-
err = nil
142-
}
143-
if err != nil {
144-
errs = append(errs,
145-
trace.Wrap(err, "failed to fetch bucket %q tags", aws.ToString(bucket.Name)),
146-
)
14797
failedReqs.failedTags = true
98+
newBucket := awsS3Bucket(aws.ToString(bucket.Name), nil, nil, nil, nil, a.AccountID)
99+
collect(mergeS3Protos(existingBucket, newBucket, failedReqs), trace.NewAggregate(errs...))
100+
return nil
148101
}
149102

150-
newBucket := awsS3Bucket(aws.ToString(bucket.Name), policy, policyStatus, acls, tagsOutput, a.AccountID)
151-
collect(mergeS3Protos(existingBucket, newBucket, failedReqs), trace.NewAggregate(errs...))
103+
details, failedReqs, errsL := a.getS3BucketDetails(ctx, bucket, bucketRegion)
104+
105+
newBucket := awsS3Bucket(aws.ToString(bucket.Name), details.policy, details.policyStatus, details.acls, details.tags, a.AccountID)
106+
collect(mergeS3Protos(existingBucket, newBucket, failedReqs), trace.NewAggregate(append(errs, errsL...)...))
152107
return nil
153108
})
154109
}
@@ -212,6 +167,7 @@ type failedRequests struct {
212167
failedPolicyStatus bool
213168
failedAcls bool
214169
failedTags bool
170+
headFailed bool
215171
}
216172

217173
func mergeS3Protos(existing, new *accessgraphv1alpha.AWSS3BucketV1, failedReqs failedRequests) *accessgraphv1alpha.AWSS3BucketV1 {
@@ -237,3 +193,124 @@ func mergeS3Protos(existing, new *accessgraphv1alpha.AWSS3BucketV1, failedReqs f
237193

238194
return clone
239195
}
196+
197+
type s3Details struct {
198+
policy *s3.GetBucketPolicyOutput
199+
policyStatus *s3.GetBucketPolicyStatusOutput
200+
acls *s3.GetBucketAclOutput
201+
tags *s3.GetBucketTaggingOutput
202+
}
203+
204+
func (a *awsFetcher) getS3BucketDetails(ctx context.Context, bucket *s3.Bucket, bucketRegion string) (s3Details, failedRequests, []error) {
205+
var failedReqs failedRequests
206+
var errs []error
207+
var details s3Details
208+
209+
s3Client, err := a.CloudClients.GetAWSS3Client(
210+
ctx,
211+
bucketRegion,
212+
a.getAWSOptions()...,
213+
)
214+
if err != nil {
215+
errs = append(errs,
216+
trace.Wrap(err, "failed to create s3 client for bucket %q", aws.ToString(bucket.Name)),
217+
)
218+
return s3Details{},
219+
failedRequests{
220+
headFailed: true,
221+
policyFailed: true,
222+
failedPolicyStatus: true,
223+
failedAcls: true,
224+
failedTags: true,
225+
}, errs
226+
}
227+
228+
details.policy, err = s3Client.GetBucketPolicyWithContext(ctx, &s3.GetBucketPolicyInput{
229+
Bucket: bucket.Name,
230+
})
231+
if err != nil && !isS3BucketPolicyNotFound(err) {
232+
errs = append(errs,
233+
trace.Wrap(err, "failed to fetch bucket %q inline policy", aws.ToString(bucket.Name)),
234+
)
235+
failedReqs.policyFailed = true
236+
}
237+
238+
details.policyStatus, err = s3Client.GetBucketPolicyStatusWithContext(ctx, &s3.GetBucketPolicyStatusInput{
239+
Bucket: bucket.Name,
240+
})
241+
if err != nil && !isS3BucketPolicyNotFound(err) {
242+
errs = append(errs,
243+
trace.Wrap(err, "failed to fetch bucket %q policy status", aws.ToString(bucket.Name)),
244+
)
245+
failedReqs.failedPolicyStatus = true
246+
}
247+
248+
details.acls, err = s3Client.GetBucketAclWithContext(ctx, &s3.GetBucketAclInput{
249+
Bucket: bucket.Name,
250+
})
251+
if err != nil {
252+
errs = append(errs,
253+
trace.Wrap(err, "failed to fetch bucket %q acls policies", aws.ToString(bucket.Name)),
254+
)
255+
failedReqs.failedAcls = true
256+
}
257+
258+
details.tags, err = s3Client.GetBucketTaggingWithContext(ctx, &s3.GetBucketTaggingInput{
259+
Bucket: bucket.Name,
260+
})
261+
if err != nil && !isS3BucketNoTagSet(err) {
262+
errs = append(errs,
263+
trace.Wrap(err, "failed to fetch bucket %q tags", aws.ToString(bucket.Name)),
264+
)
265+
failedReqs.failedTags = true
266+
}
267+
268+
return details, failedReqs, errs
269+
}
270+
271+
func isS3BucketPolicyNotFound(err error) bool {
272+
var awsErr awserr.Error
273+
return errors.As(err, &awsErr) && awsErr.Code() == "NoSuchBucketPolicy"
274+
}
275+
276+
func isS3BucketNoTagSet(err error) bool {
277+
var awsErr awserr.Error
278+
return errors.As(err, &awsErr) && awsErr.Code() == "NoSuchTagSet"
279+
}
280+
281+
func (a *awsFetcher) listS3Buckets(ctx context.Context) ([]*s3.Bucket, func(*string) (string, error), error) {
282+
region := awsutil.GetKnownRegions()[0]
283+
if len(a.Regions) > 0 {
284+
region = a.Regions[0]
285+
}
286+
287+
// use any region to list buckets
288+
s3Client, err := a.CloudClients.GetAWSS3Client(
289+
ctx,
290+
region,
291+
a.getAWSOptions()...,
292+
)
293+
if err != nil {
294+
return nil, nil, trace.Wrap(err)
295+
}
296+
rsp, err := s3Client.ListBucketsWithContext(ctx, &s3.ListBucketsInput{})
297+
if err != nil {
298+
return nil, nil, trace.Wrap(err)
299+
}
300+
return rsp.Buckets,
301+
func(bucket *string) (string, error) {
302+
rsp, err := s3Client.GetBucketLocationWithContext(
303+
ctx,
304+
&s3.GetBucketLocationInput{
305+
Bucket: bucket,
306+
},
307+
)
308+
if err != nil {
309+
return "", trace.Wrap(err, "failed to fetch bucket %q region", aws.ToString(bucket))
310+
}
311+
if rsp.LocationConstraint == nil {
312+
return "us-east-1", nil
313+
}
314+
return aws.ToString(rsp.LocationConstraint), nil
315+
}, nil
316+
}

lib/srv/discovery/fetchers/aws-sync/s3_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ func TestPollAWSS3(t *testing.T) {
5555
mockedClients = &cloud.TestCloudClients{
5656
S3: &mocks.S3Mock{
5757
Buckets: s3Buckets(bucketName, otherBucketName),
58+
BucketLocations: map[string]string{
59+
bucketName: "eu-west-1",
60+
otherBucketName: "eu-west-1",
61+
},
5862
BucketPolicy: map[string]string{
5963
bucketName: "policy",
6064
otherBucketName: "otherPolicy",

0 commit comments

Comments
 (0)