From a406ba94b6f5046bc4b3b7629f465c9c7ed61f97 Mon Sep 17 00:00:00 2001 From: Pat Gavlin Date: Tue, 5 Sep 2023 13:01:52 -0700 Subject: [PATCH] [google] Add a Bytes credentials source These changes add a credentials source to the google package that accepts an external account subject token as literal bytes (rather than e.g. a path to a file). This is useful for scenarios where using the filesystem is undesirable. --- .../externalaccount/basecredentials.go | 4 + .../externalaccount/bytescredsource.go | 45 +++++++++++ .../externalaccount/bytescredsource_test.go | 80 +++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 google/internal/externalaccount/bytescredsource.go create mode 100644 google/internal/externalaccount/bytescredsource_test.go diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index dcd252a61..46a321882 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -144,6 +144,8 @@ type format struct { // One field amongst File, URL, and Executable should be filled, depending on the kind of credential in question. // The EnvironmentID should start with AWS if being used for an AWS credential. type CredentialSource struct { + Bytes []byte `json:"bytes"` + File string `json:"file"` URL string `json:"url"` @@ -191,6 +193,8 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { return awsCredSource, nil } + } else if len(c.CredentialSource.Bytes) != 0 { + return bytesCredentialSource{Bytes: c.CredentialSource.Bytes, Format: c.CredentialSource.Format}, nil } else if c.CredentialSource.File != "" { return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil } else if c.CredentialSource.URL != "" { diff --git a/google/internal/externalaccount/bytescredsource.go b/google/internal/externalaccount/bytescredsource.go new file mode 100644 index 000000000..b3d993e84 --- /dev/null +++ b/google/internal/externalaccount/bytescredsource.go @@ -0,0 +1,45 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE bytes. + +package externalaccount + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" +) + +type bytesCredentialSource struct { + Bytes []byte + Format format +} + +func (cs bytesCredentialSource) subjectToken() (string, error) { + tokenBytes := bytes.TrimSpace(cs.Bytes) + var err error + switch cs.Format.Type { + case "json": + jsonData := make(map[string]interface{}) + err = json.Unmarshal(tokenBytes, &jsonData) + if err != nil { + return "", fmt.Errorf("oauth2/google: failed to unmarshal subject token bytes: %v", err) + } + val, ok := jsonData[cs.Format.SubjectTokenFieldName] + if !ok { + return "", errors.New("oauth2/google: provided subject_token_field_name not found in credentials") + } + token, ok := val.(string) + if !ok { + return "", errors.New("oauth2/google: improperly formatted subject token") + } + return token, nil + case "text": + return string(tokenBytes), nil + case "": + return string(tokenBytes), nil + default: + return "", errors.New("oauth2/google: invalid credential_source bytes format type") + } +} diff --git a/google/internal/externalaccount/bytescredsource_test.go b/google/internal/externalaccount/bytescredsource_test.go new file mode 100644 index 000000000..dac3e5631 --- /dev/null +++ b/google/internal/externalaccount/bytescredsource_test.go @@ -0,0 +1,80 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "os" + "testing" +) + +var testBytesConfig = Config{ + Audience: "32555940559.apps.googleusercontent.com", + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", + TokenURL: "http://localhost:8080/v1/token", + TokenInfoURL: "http://localhost:8080/v1/tokeninfo", + ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-gcs-admin@$PROJECT_ID.iam.gserviceaccount.com:generateAccessToken", + ClientSecret: "notsosecret", + ClientID: "rbrgnognrhongo3bi4gb9ghg9g", +} + +func TestRetrieveBytesSubjectToken(t *testing.T) { + var fileSourceTests = []struct { + name string + cs CredentialSource + want string + }{ + { + name: "UntypedFileSource", + cs: CredentialSource{ + File: textBaseCredPath, + }, + want: "street123", + }, + { + name: "TextFileSource", + cs: CredentialSource{ + File: textBaseCredPath, + Format: format{Type: fileTypeText}, + }, + want: "street123", + }, + { + name: "JSONFileSource", + cs: CredentialSource{ + File: jsonBaseCredPath, + Format: format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"}, + }, + want: "321road", + }, + } + + for _, test := range fileSourceTests { + test := test + tbc := testBytesConfig + tbc.CredentialSource = test.cs + + bytes, err := os.ReadFile(test.cs.File) + if err != nil { + t.Fatalf("failed to read subject token: %v", err) + } + tbc.CredentialSource.Bytes = bytes + + t.Run(test.name, func(t *testing.T) { + base, err := tbc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() + if err != nil { + t.Errorf("Method subjectToken() errored.") + } else if test.want != out { + t.Errorf("got %v but want %v", out, test.want) + } + + }) + } +}