Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

blob/gcsblob: add support for forcing an authenticated client #3273

Merged
merged 1 commit into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions blob/gcsblob/gcsblob.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
// The default URL opener will set up a connection using default credentials
// from the environment, as described in
// https://cloud.google.com/docs/authentication/production.
// You may force the use of an unauthenticated client by setting
// GoogleAccessID to "-" (via Options or via the URL parameter "access_id").
// Some environments, such as GCE, come without a private key. In such cases
// the IAM Credentials API will be configured for use in Options.MakeSignBytes,
// which will introduce latency to any and all calls to bucket.SignedURL
Expand Down Expand Up @@ -206,7 +208,8 @@ const Scheme = "gs"
// - access_id: sets Options.GoogleAccessID
// - private_key_path: path to read for Options.PrivateKey
//
// Currently their use is limited to SignedURL.
// Currently their use is limited to SignedURL, except that setting access_id
// to "-" forces the use of an unauthenticated client.
type URLOpener struct {
// Client must be set to a non-nil HTTP client authenticated with
// Cloud Storage scope or equivalent.
Expand All @@ -218,32 +221,34 @@ type URLOpener struct {

// OpenBucketURL opens the GCS bucket with the same name as the URL's host.
func (o *URLOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket, error) {
opts, err := o.forParams(ctx, u.Query())
opts, client, err := o.forParams(ctx, u.Query())
if err != nil {
return nil, fmt.Errorf("open bucket %v: %v", u, err)
}
return OpenBucket(ctx, o.Client, u.Host, opts)
return OpenBucket(ctx, client, u.Host, opts)
}

func (o *URLOpener) forParams(ctx context.Context, q url.Values) (*Options, error) {
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" {
return nil, fmt.Errorf("invalid query parameter %q", k)
return nil, nil, fmt.Errorf("invalid query parameter %q", k)
}
}
opts := new(Options)
*opts = o.Options
client := o.Client
if accessID := q.Get("access_id"); accessID != "" && accessID != opts.GoogleAccessID {
opts.GoogleAccessID = accessID
opts.PrivateKey = nil // Clear any previous key unrelated to the new accessID.

// Clear this as well to prevent calls with the old and mismatched accessID.
opts.MakeSignBytes = nil
opts.clear()
if accessID == "-" {
client = gcp.NewAnonymousHTTPClient(gcp.DefaultTransport())
} else {
opts.GoogleAccessID = accessID
}
}
if keyPath := q.Get("private_key_path"); keyPath != "" {
pk, err := ioutil.ReadFile(keyPath)
if err != nil {
return nil, err
return nil, nil, err
}
opts.PrivateKey = pk
} else if _, exists := q["private_key_path"]; exists {
Expand All @@ -252,12 +257,13 @@ func (o *URLOpener) forParams(ctx context.Context, q url.Values) (*Options, erro
// is intentional such as for tests or involving a key stored in a HSM/TPM.
opts.PrivateKey = nil
}
return opts, nil
return opts, client, nil
}

// Options sets options for constructing a *blob.Bucket backed by GCS.
type Options struct {
// GoogleAccessID represents the authorizer for SignedURL.
// If set to "-", an unauthenticated client will be used.
// Required to use SignedURL.
// See https://godoc.org/cloud.google.com/go/storage#SignedURLOptions.
GoogleAccessID string
Expand All @@ -279,6 +285,14 @@ type Options struct {
MakeSignBytes func(requestCtx context.Context) SignBytesFunc
}

// clear clears all the fields of o.
func (o *Options) clear() {
o.GoogleAccessID = ""
o.PrivateKey = nil
o.SignBytes = nil
o.MakeSignBytes = nil
}

// SignBytesFunc is shorthand for the signature of Options.SignBytes.
type SignBytesFunc func([]byte) ([]byte, error)

Expand Down
25 changes: 19 additions & 6 deletions blob/gcsblob/gcsblob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,11 +532,12 @@ func TestURLOpenerForParams(t *testing.T) {
}

tests := []struct {
name string
currOpts Options
query url.Values
wantOpts Options
wantErr bool
name string
currOpts Options
query url.Values
wantOpts Options
wantClient bool
wantErr bool
}{
{
name: "InvalidParam",
Expand All @@ -560,6 +561,15 @@ func TestURLOpenerForParams(t *testing.T) {
},
wantOpts: Options{GoogleAccessID: "bar"},
},
{
name: "AccessID override to - makes new client",
currOpts: Options{GoogleAccessID: "foo"},
query: url.Values{
"access_id": {"-"},
},
wantOpts: Options{}, // cleared
wantClient: true,
},
{
name: "AccessID not overridden",
currOpts: Options{GoogleAccessID: "bar"},
Expand Down Expand Up @@ -608,7 +618,7 @@ func TestURLOpenerForParams(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
o := &URLOpener{Options: test.currOpts}
got, err := o.forParams(ctx, test.query)
got, gotClient, err := o.forParams(ctx, test.query)
if (err != nil) != test.wantErr {
t.Errorf("got err %v want error %v", err, test.wantErr)
}
Expand All @@ -618,6 +628,9 @@ func TestURLOpenerForParams(t *testing.T) {
if diff := cmp.Diff(got, &test.wantOpts); diff != "" {
t.Errorf("opener.forParams(...) diff (-want +got):\n%s", diff)
}
if test.wantClient != (gotClient != nil) {
t.Errorf("opener.forParams client return value was unexpected, got %v want %v", gotClient != nil, test.wantClient)
}
})
}
}
Expand Down
Loading