diff --git a/pkg/fileservice/aws_sdk_v2.go b/pkg/fileservice/aws_sdk_v2.go index 422ace576bede..959fbf7e52163 100644 --- a/pkg/fileservice/aws_sdk_v2.go +++ b/pkg/fileservice/aws_sdk_v2.go @@ -25,6 +25,7 @@ import ( "math" nethttp "net/http" "net/url" + "os" gotrace "runtime/trace" "slices" "strings" @@ -1179,8 +1180,18 @@ func (a *AwsSDKv2) mapError(err error, path string) error { // explicitAWSConfig never consults process environment or shared configuration. // S3 and STS share this mode so role refresh cannot rediscover another principal. func explicitAWSConfig(o ObjectStorageArguments, provider aws.CredentialsProvider, client *nethttp.Client) aws.Config { + region := o.Region + if region == "" { + // SQL dynamic providers must not discover credentials from the server + // environment, but the region is non-secret configuration. Preserve the + // AWS SDK's environment-region fallback for existing S3-compatible URLs. + region = os.Getenv("AWS_REGION") + if region == "" { + region = os.Getenv("AWS_DEFAULT_REGION") + } + } return aws.Config{ - Region: o.Region, Credentials: provider, HTTPClient: client, + Region: region, Credentials: provider, HTTPClient: client, Logger: logutil.GetS3Logger(), Retryer: func() aws.Retryer { return newAWSRetryer() }, RequestChecksumCalculation: aws.RequestChecksumCalculationWhenRequired, ResponseChecksumValidation: aws.ResponseChecksumValidationWhenRequired, diff --git a/pkg/fileservice/sql_file_access_test.go b/pkg/fileservice/sql_file_access_test.go index 1085872e0f11e..02f2c237a45f4 100644 --- a/pkg/fileservice/sql_file_access_test.go +++ b/pkg/fileservice/sql_file_access_test.go @@ -173,6 +173,50 @@ func TestSQLFileCredentialsDoNotDiscoverServerIdentity(t *testing.T) { require.Equal(t, 4, calls) } +func TestSQLFileS3UsesEnvironmentRegionWhenNotSpecified(t *testing.T) { + t.Setenv("AWS_REGION", "us-east-2") + t.Setenv("AWS_DEFAULT_REGION", "us-west-1") + t.Setenv("AWS_ACCESS_KEY_ID", "ambient-key") + t.Setenv("AWS_SECRET_ACCESS_KEY", "ambient-secret") + old := httpRoundTripper + t.Cleanup(func() { httpRoundTripper = old }) + calls := 0 + httpRoundTripper = sqlCredentialTransport(func(r *http.Request) (*http.Response, error) { + calls++ + assert.Equal(t, http.MethodHead, r.Method) + authorization := r.Header.Get("Authorization") + assert.Contains(t, authorization, "Credential=caller-key/") + assert.Contains(t, authorization, "/us-east-2/s3/aws4_request") + assert.NotContains(t, authorization, "ambient") + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Length": []string{"0"}, "Last-Modified": []string{"Mon, 02 Jan 2006 15:04:05 GMT"}, "Etag": []string{`"owned"`}}, + Body: io.NopCloser(strings.NewReader("")), + Request: r, + }, nil + }) + + ctx := WithSQLFileAccess(t.Context(), "tenant", "public") + spec := "s3-opts,endpoint=https://owned.invalid,bucket=owned-bucket-123456,key=caller-key,secret=caller-secret:object" + fs, path, err := GetForETL(ctx, nil, spec) + require.NoError(t, err) + defer fs.Close(ctx) + _, err = fs.StatFile(ctx, path) + require.NoError(t, err) + require.Equal(t, 1, calls) +} + +func TestExplicitAWSConfigRegionFallback(t *testing.T) { + t.Setenv("AWS_REGION", "us-east-2") + t.Setenv("AWS_DEFAULT_REGION", "us-west-1") + require.Equal(t, "us-east-2", explicitAWSConfig(ObjectStorageArguments{}, nil, nil).Region) + + t.Setenv("AWS_REGION", "") + require.Equal(t, "us-west-1", explicitAWSConfig(ObjectStorageArguments{}, nil, nil).Region) + + require.Equal(t, "eu-west-1", explicitAWSConfig(ObjectStorageArguments{Region: "eu-west-1"}, nil, nil).Region) +} + func TestSQLFileRoleFailureCannotFallBack(t *testing.T) { old := httpRoundTripper t.Cleanup(func() { httpRoundTripper = old })