Repository: google/go-cloud (go)

Issue:
s3blob: Need a way to set custom credentials provider for S3 when using v2
### Describe the bug

The aws-sdk-go (v1) module is being removed soon, and so I'm trying to migrate to aws-sdk-go-v2 (see also #3489).

I have a need to sometimes use the anonymous CredentialsProvider, and with V1 I could achieve this by setting the `ConfigProvider` field with an AWS session properly configured with the anonymous provider.

I'm using the `blob.BucketURLOpener` interface to support non-S3 buckets as well via URLs, and so using `s3blob.OpenBucketV2` with a custom client isn't an option.

### To Reproduce

Attempt to use `&s3blob.URLOpener{UseV2: true}` to open S3 buckets with anonymous credential...

## Expected behavior

There are a few ways to achieve this - one way would be to allow adding additional `config.LoadOptions` items to be provided to `s3blob.V2ConfigFromURLParams` via a new field in the `s3blob.URLOpener` struct.

Alternately, a new URL query parameter could be supported - something like `anonymous=true`. FWIW, that would be useful in the `gcsblob` package as well.

### Version

v0.40.0 (latest)

### Additional context

n/a

Pull request:
aws, blob: Add support for anonymous query param for gcsblob and all AWSv2 providers
Fixes #3512.

Patch:
diff --git a/aws/aws.go b/aws/aws.go
index 4ee177c3..a62c08fe 100644
--- a/aws/aws.go
+++ b/aws/aws.go
@@ -195,6 +195,7 @@ func NewDefaultV2Config(ctx context.Context) (awsv2.Config, error) {
 //
 // The following query options are supported:
 //   - region: The AWS region for requests; sets WithRegion.
+//   - anonymous: A value of "true" forces use of anonymous credentials.
 //   - profile: The shared config profile to use; sets SharedConfigProfile.
 //   - endpoint: The AWS service endpoint to send HTTP request.
 //   - hostname_immutable: Make the hostname immutable, only works if endpoint is also set.
@@ -245,6 +246,14 @@ func V2ConfigFromURLParams(ctx context.Context, q url.Values) (awsv2.Config, err
 			if err != nil {
 				return awsv2.Config{}, fmt.Errorf("invalid value for capacity: %w", err)
 			}
+		case "anonymous":
+			anon, err := strconv.ParseBool(value)
+			if err != nil {
+				return awsv2.Config{}, fmt.Errorf("invalid value for anonymous: %w", err)
+			}
+			if anon {
+				opts = append(opts, awsv2cfg.WithCredentialsProvider(awsv2.AnonymousCredentials{}))
+			}
 		case "awssdk":
 			// ignore, should be handled before this
 		default:
diff --git a/blob/gcsblob/gcsblob.go b/blob/gcsblob/gcsblob.go
index 55e3091b..acb3a0da 100644
--- a/blob/gcsblob/gcsblob.go
+++ b/blob/gcsblob/gcsblob.go
@@ -67,6 +67,7 @@ import (
 	"net/url"
 	"os"
 	"sort"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -206,14 +207,13 @@ const Scheme = "gs"
 //
 // The following query parameters are supported:
 //
-//   - access_id: sets Options.GoogleAccessID
-//   - private_key_path: path to read for Options.PrivateKey
-//
-// Currently their use is limited to SignedURL, except that setting access_id
-// to "-" forces the use of an unauthenticated client.
+//   - anonymous: A value of "true" forces the use of an unauthenticated client.
+//   - access_id: Sets Options.GoogleAccessID; only used in SignedURL, except that
+//     a value of "-" forces the use of an unauthenticated client.
+//   - private_key_path: Path to read for Options.PrivateKey; only used in SignedURL.
 type URLOpener struct {
 	// Client must be set to a non-nil HTTP client authenticated with
-	// Cloud Storage scope or equivalent.
+	// Cloud Storage scope or equivalent (unless anonymous=true).
 	Client *gcp.HTTPClient
 
 	// Options specifies the default options to pass to OpenBucket.
@@ -231,13 +231,23 @@ func (o *URLOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket
 
 func (o *URLOpener) forParams(ctx context.Context, q url.Values) (*Options, *gcp.HTTPClient, error) {
 	for k := range q {
-		if k != "access_id" && k != "private_key_path" {
+		if k != "access_id" && k != "private_key_path" && k != "anonymous" {
 			return nil, nil, fmt.Errorf("invalid query parameter %q", k)
 		}
 	}
 	opts := new(Options)
 	*opts = o.Options
 	client := o.Client
+	if anon := q.Get("anonymous"); anon != "" {
+		isAnon, err := strconv.ParseBool(anon)
+		if err != nil {
+			return nil, nil, fmt.Errorf("invalid value %q for query parameter \"anonymous\": %w", anon, err)
+		}
+		if isAnon {
+			opts.clear()
+			client = gcp.NewAnonymousHTTPClient(gcp.DefaultTransport())
+		}
+	}
 	if accessID := q.Get("access_id"); accessID != "" && accessID != opts.GoogleAccessID {
 		opts.clear()
 		if accessID == "-" {


Test patch:
diff --git a/aws/aws_test.go b/aws/aws_test.go
index e48e16aa..e1780689 100644
--- a/aws/aws_test.go
+++ b/aws/aws_test.go
@@ -199,6 +199,10 @@ func TestV2ConfigFromURLParams(t *testing.T) {
 			name:  "FIPS and dual stack",
 			query: url.Values{"fips": {"true"}, "dualstack": {"true"}},
 		},
+		{
+			name:  "anonymous",
+			query: url.Values{"anonymous": {"true"}},
+		},
 		{
 			name:  "Rate limit capacity",
 			query: url.Values{"rate_limiter_capacity": {"500"}},
diff --git a/blob/gcsblob/gcsblob_test.go b/blob/gcsblob/gcsblob_test.go
index 7d49fc47..6a3dca81 100644
--- a/blob/gcsblob/gcsblob_test.go
+++ b/blob/gcsblob/gcsblob_test.go
@@ -577,6 +577,22 @@ func TestURLOpenerForParams(t *testing.T) {
 			currOpts: Options{GoogleAccessID: "bar"},
 			wantOpts: Options{GoogleAccessID: "bar"},
 		},
+		{
+			name: "Invalid value for anonymous",
+			query: url.Values{
+				"anonymous": {"bad"},
+			},
+			wantErr: true,
+		},
+		{
+			name:     "Anonymous",
+			currOpts: Options{GoogleAccessID: "foo"},
+			query: url.Values{
+				"anonymous": {"true"},
+			},
+			wantOpts:   Options{}, // cleared
+			wantClient: true,
+		},
 		{
 			name: "BadPrivateKeyPath",
 			query: url.Values{
diff --git a/blob/s3blob/s3blob_test.go b/blob/s3blob/s3blob_test.go
index 508964ab..d0e0573f 100644
--- a/blob/s3blob/s3blob_test.go
+++ b/blob/s3blob/s3blob_test.go
@@ -490,6 +490,8 @@ func TestOpenBucketFromURL(t *testing.T) {
 		{"s3://mybucket?disable_https=true", false},
 		// OK, use FIPS endpoints (v1)
 		{"s3://mybucket?awssdk=v1&fips=true", false},
+		// OK, use anonymous.
+		{"s3://mybucket?awssdk=v2&anonymous=true", false},
 		// Invalid accelerate (v1)
 		{"s3://mybucket?awssdk=v1&accelerate=bogus", true},
 		// Invalid accelerate (v2)